UI: switch to Syncfusion M3, remove Tailwind;
paginate changelog; docs updated; bump to 2025.1.0-alpha.8
This commit is contained in:
20
.github/copilot-instructions.md
vendored
20
.github/copilot-instructions.md
vendored
@@ -60,10 +60,20 @@ Use this as your shared context when proposing changes. Keep edits minimal and m
|
||||
- Vite React app; proxies `/api` and `/screenshots` to API in dev (`vite.config.ts`).
|
||||
- Uses Syncfusion components; Vite config pre-bundles specific packages to avoid alias issues.
|
||||
- Environment: `VITE_API_URL` provided at build/run; in dev compose, proxy handles `/api` so local fetches can use relative `/api/...` paths.
|
||||
- Theming: Syncfusion Material 3 theme is used. All component CSS is imported centrally in `dashboard/src/main.tsx` (base, navigations, buttons, inputs, dropdowns, popups, kanban, grids, schedule, filemanager, notifications, layouts, lists, calendars, splitbuttons, icons). Tailwind CSS has been removed.
|
||||
- Scheduler (appointments page): top bar includes Group and Academic Period selectors (Syncfusion DropDownList). Selecting a period calls `POST /api/academic_periods/active`, moves the calendar to today’s month/day within the period year, and refreshes a right-aligned indicator row showing:
|
||||
- Holidays present in the current view (count)
|
||||
- Period label (display_name or name) with a badge indicating whether any holidays exist in that period (overlap check)
|
||||
|
||||
- Program info page (`dashboard/src/programminfo.tsx`):
|
||||
- Loads data from `dashboard/public/program-info.json` (app name, version, build info, tech stack, changelog).
|
||||
- Uses Syncfusion card classes (`e-card`, `e-card-header`, `e-card-title`, `e-card-content`) for consistent styling.
|
||||
- Changelog is paginated with `PagerComponent` (from `@syncfusion/ej2-react-grids`), default page size 5; adjust `pageSize` or add a selector as needed.
|
||||
|
||||
- Groups page (`dashboard/src/infoscreen_groups.tsx`):
|
||||
- Migrated to Syncfusion inputs and popups: Buttons, TextBox, DropDownList, Dialog; Kanban remains for drag/drop.
|
||||
- Unified toast/dialog wording; replaced legacy alerts with toasts; spacing handled via inline styles to avoid Tailwind dependency.
|
||||
|
||||
Note: Syncfusion usage in the dashboard is already documented above; if a UI for conversion status/downloads is added later, link its routes and components here.
|
||||
|
||||
## Local development
|
||||
@@ -116,3 +126,13 @@ Questions or unclear areas? Tell us if you need: exact devcontainer debugging st
|
||||
- **Usage**: New events/media can optionally reference `academic_period_id` for better organization and filtering.
|
||||
- **Constraints**: Only one period can be active at a time; use `init_academic_periods.py` for Austrian school year setup.
|
||||
- **UI Integration**: The dashboard highlights the currently selected period and whether a holiday plan exists within that date range. Holiday linkage currently uses date overlap with `school_holidays`; an explicit `academic_period_id` on `school_holidays` can be added later if tighter association is required.
|
||||
|
||||
## Changelog Style Guide (Program info)
|
||||
|
||||
- Source: `dashboard/public/program-info.json`; newest entry first
|
||||
- Fields per release: `version`, `date` (YYYY-MM-DD), `changes` (array of short bullets)
|
||||
- Tone: concise, user-facing; German wording; area prefixes allowed (e.g., “UI: …”, “API: …”)
|
||||
- Categories via emoji or words: Added (🆕/✨), Changed (🛠️), Fixed (✅/🐛), Removed (🗑️), Security (🔒), Deprecated (⚠️)
|
||||
- Breaking changes must be prefixed with `BREAKING:`
|
||||
- Keep ≤ 8–10 bullets; summarize or group micro-changes
|
||||
- JSON hygiene: valid JSON, no trailing commas, don’t edit historical entries except typos
|
||||
|
||||
40
README.md
40
README.md
@@ -132,7 +132,8 @@ For detailed deployment instructions, see:
|
||||
|
||||
### 🖥️ **Dashboard** (`dashboard/`)
|
||||
- **Technology**: React 19 + TypeScript + Vite
|
||||
- **UI Framework**: Syncfusion components + Tailwind CSS
|
||||
- **UI Framework**: Syncfusion components (Material 3 theme)
|
||||
- **Styling**: Centralized Syncfusion Material 3 CSS imports in `dashboard/src/main.tsx`
|
||||
- **Features**: Responsive design, real-time updates, file management
|
||||
- **Port**: 5173 (dev), served via Nginx (prod)
|
||||
|
||||
@@ -270,13 +271,15 @@ mosquitto_sub -h localhost -t "infoscreen/+/heartbeat" -v
|
||||
|
||||
## 🎨 Frontend Features
|
||||
|
||||
### Syncfusion Components Used
|
||||
### Syncfusion Components Used (Material 3)
|
||||
- **Schedule**: Event calendar with drag-drop support
|
||||
- **Grid**: Data tables with filtering and sorting
|
||||
- **DropDownList**: Group and period selectors
|
||||
- **FileManager**: Media upload and organization
|
||||
- **Kanban**: Task management views
|
||||
- **Notifications**: Toast messages and alerts
|
||||
- **Pager**: Used on Programinfo changelog for pagination
|
||||
- **Cards (layouts)**: Programinfo sections styled with Syncfusion card classes
|
||||
|
||||
### Pages Overview
|
||||
- **Dashboard**: System overview and statistics
|
||||
@@ -286,6 +289,7 @@ mosquitto_sub -h localhost -t "infoscreen/+/heartbeat" -v
|
||||
- **Media**: File upload and conversion
|
||||
- **Settings**: System configuration
|
||||
- **Holidays**: Academic calendar management
|
||||
- **Program info**: Version, build info, tech stack and paginated changelog (reads `dashboard/public/program-info.json`)
|
||||
|
||||
## 🔒 Security & Authentication
|
||||
|
||||
@@ -415,3 +419,35 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
||||
---
|
||||
|
||||
For detailed technical documentation, deployment guides, and API specifications, please refer to the additional documentation files in this repository.
|
||||
|
||||
Notes:
|
||||
- Tailwind CSS was removed. Styling is managed via Syncfusion Material 3 theme imports in `dashboard/src/main.tsx`.
|
||||
|
||||
## 🧭 Changelog Style Guide
|
||||
|
||||
When adding entries to `dashboard/public/program-info.json` (displayed on the Program info page):
|
||||
|
||||
- Structure per release
|
||||
- `version` (e.g., `2025.1.0-alpha.8`)
|
||||
- `date` in `YYYY-MM-DD` (ISO format)
|
||||
- `changes`: array of short bullet strings
|
||||
|
||||
- Categories (Keep a Changelog inspired)
|
||||
- Prefer starting bullets with an implicit category or an emoji, e.g.:
|
||||
- Added (🆕/✨), Changed (🔧/🛠️), Fixed (🐛/✅), Removed (🗑️), Security (🔒), Deprecated (⚠️)
|
||||
|
||||
- Writing rules
|
||||
- Keep bullets concise (ideally one line) and user-facing; avoid internal IDs or jargon
|
||||
- Put the affected area first when helpful (e.g., “UI: …”, “API: …”, “Scheduler: …”)
|
||||
- Highlight breaking changes with “BREAKING:”
|
||||
- Prefer German wording consistently; dates are localized at runtime for display
|
||||
|
||||
- Ordering and size
|
||||
- Newest release first in the array
|
||||
- Aim for ≤ 8–10 bullets per release; group or summarize if longer
|
||||
|
||||
- JSON hygiene
|
||||
- Valid JSON only (no trailing commas); escape quotes as needed
|
||||
- One release object per version; do not modify historical entries unless to correct typos
|
||||
|
||||
The Program info page paginates older entries (default page size 5). Keep highlights at the top of each release for scanability.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-tailwindcss"
|
||||
"stylelint-config-standard"
|
||||
],
|
||||
"rules": {
|
||||
"at-rule-no-unknown": null
|
||||
|
||||
@@ -45,9 +45,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
@@ -66,8 +63,6 @@
|
||||
"prettier": "^3.5.3",
|
||||
"stylelint": "^16.21.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"stylelint-config-tailwindcss": "^1.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"appName": "Infoscreen-Management",
|
||||
"version": "2025.1.0-alpha.7",
|
||||
"version": "2025.1.0-alpha.8",
|
||||
"copyright": "© 2025 Third-Age-Applications",
|
||||
"supportContact": "support@third-age-applications.com",
|
||||
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
|
||||
@@ -30,6 +30,17 @@
|
||||
"commitId": "8d1df7199cb7"
|
||||
},
|
||||
"changelog": [
|
||||
{
|
||||
"version": "2025.1.0-alpha.8",
|
||||
"date": "2025-10-11",
|
||||
"changes": [
|
||||
"🎨 Theme: Umstellung auf Syncfusion Material 3; zentrale CSS-Imports in main.tsx",
|
||||
"🧹 Cleanup: Tailwind CSS komplett entfernt (Pakete, PostCSS, Stylelint, Konfigurationsdateien)",
|
||||
"🧩 Gruppenverwaltung: \"infoscreen_groups\" auf Syncfusion-Komponenten (Buttons, Dialoge, DropDownList, TextBox) umgestellt; Abstände verbessert",
|
||||
"🔔 Benachrichtigungen: Vereinheitlichte Toast-/Dialog-Texte; letzte Alert-Verwendung ersetzt",
|
||||
"📖 Doku: README und Copilot-Anweisungen angepasst (Material 3, zentrale Styles, kein Tailwind)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2025.1.0-alpha.7",
|
||||
"date": "2025-09-21",
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
@import "../node_modules/@syncfusion/ej2-base/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-buttons/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-calendars/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-dropdowns/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-inputs/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-lists/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-navigations/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-popups/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-splitbuttons/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-react-schedule/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-kanban/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-notifications/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-react-filemanager/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-layouts/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-grids/styles/material.css";
|
||||
@import "../node_modules/@syncfusion/ej2-icons/styles/material.css";
|
||||
/* Removed legacy Syncfusion material theme imports; using material3 imports in main.tsx */
|
||||
|
||||
body {
|
||||
font-family: Inter, 'Segoe UI', Roboto, Arial, sans-serif;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
/* @tailwind base;
|
||||
@tailwind components; */
|
||||
|
||||
/* @tailwind utilities; */
|
||||
/* Tailwind removed: base/components/utilities directives no longer used. */
|
||||
|
||||
/* :root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
|
||||
@@ -5,6 +5,11 @@ import { fetchGroups, createGroup, deleteGroup, renameGroup } from './apiGroups'
|
||||
import type { Client } from './apiClients';
|
||||
import type { KanbanComponent as KanbanComponentType } from '@syncfusion/ej2-react-kanban';
|
||||
import { DialogComponent } from '@syncfusion/ej2-react-popups';
|
||||
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
|
||||
import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
|
||||
import { DropDownListComponent } from '@syncfusion/ej2-react-dropdowns';
|
||||
import type { ChangedEventArgs as TextBoxChangedArgs } from '@syncfusion/ej2-react-inputs';
|
||||
import type { ChangeEventArgs as DropDownChangeArgs } from '@syncfusion/ej2-react-dropdowns';
|
||||
import { useToast } from './components/ToastProvider';
|
||||
import { L10n } from '@syncfusion/ej2-base';
|
||||
|
||||
@@ -41,10 +46,10 @@ const de = {
|
||||
rename: 'Umbenennen',
|
||||
confirmDelete: 'Löschbestätigung',
|
||||
reallyDelete: (name: string) => `Möchten Sie die Gruppe <b>${name}</b> wirklich löschen?`,
|
||||
clientsMoved: 'Alle Clients werden in "Nicht zugeordnet" verschoben.',
|
||||
clientsMoved: 'Alle Clients werden nach "Nicht zugeordnet" verschoben.',
|
||||
groupCreated: 'Gruppe angelegt',
|
||||
groupDeleted: 'Gruppe gelöscht. Clients in "Nicht zugeordnet" verschoben',
|
||||
groupRenamed: 'Gruppenname geändert',
|
||||
groupDeleted: 'Gruppe gelöscht. Alle Clients wurden nach "Nicht zugeordnet" verschoben.',
|
||||
groupRenamed: 'Gruppe umbenannt',
|
||||
selectGroup: 'Gruppe wählen',
|
||||
newName: 'Neuer Name',
|
||||
warning: 'Achtung:',
|
||||
@@ -312,7 +317,12 @@ const Infoscreen_groups: React.FC = () => {
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
alert('Fehler beim Aktualisieren der Clients');
|
||||
toast.show({
|
||||
content: 'Fehler beim Aktualisieren der Clients',
|
||||
cssClass: 'e-toast-danger',
|
||||
timeOut: 0,
|
||||
showCloseButton: true,
|
||||
});
|
||||
}
|
||||
setDraggedCard(null);
|
||||
};
|
||||
@@ -326,25 +336,23 @@ const Infoscreen_groups: React.FC = () => {
|
||||
return (
|
||||
<div id="dialog-target">
|
||||
<h2 className="text-xl font-bold mb-4">{de.title}</h2>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded"
|
||||
onClick={() => setShowDialog(true)}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '12px',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
<ButtonComponent cssClass="e-primary" onClick={() => setShowDialog(true)}>
|
||||
{de.newGroup}
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-yellow-500 text-white rounded"
|
||||
onClick={() => setRenameDialog({ open: true, oldName: '', newName: '' })}
|
||||
>
|
||||
</ButtonComponent>
|
||||
<ButtonComponent cssClass="e-warning" onClick={() => setRenameDialog({ open: true, oldName: '', newName: '' })}>
|
||||
{de.renameGroup}
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-red-500 text-white rounded"
|
||||
onClick={() => setDeleteDialog({ open: true, groupName: '' })}
|
||||
>
|
||||
</ButtonComponent>
|
||||
<ButtonComponent cssClass="e-danger" onClick={() => setDeleteDialog({ open: true, groupName: '' })}>
|
||||
{de.deleteGroup}
|
||||
</button>
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
<KanbanComponent
|
||||
locale="de"
|
||||
@@ -362,144 +370,146 @@ const Infoscreen_groups: React.FC = () => {
|
||||
columns={kanbanColumns}
|
||||
/>
|
||||
{showDialog && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
||||
<div className="bg-white p-6 rounded shadow">
|
||||
<h3 className="mb-2 font-bold">{de.newGroup}</h3>
|
||||
<input
|
||||
className="border p-2 mb-2 w-full"
|
||||
value={newGroupName}
|
||||
onChange={e => setNewGroupName(e.target.value)}
|
||||
placeholder="Raumname"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button className="bg-blue-500 text-white px-4 py-2 rounded" onClick={handleAddGroup}>
|
||||
<DialogComponent
|
||||
visible={showDialog}
|
||||
header={de.newGroup}
|
||||
close={() => setShowDialog(false)}
|
||||
target="#dialog-target"
|
||||
width="420px"
|
||||
footerTemplate={() => (
|
||||
<div className="flex gap-2 justify-end">
|
||||
<ButtonComponent cssClass="e-primary" onClick={handleAddGroup} disabled={!newGroupName.trim()}>
|
||||
{de.add}
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-300 px-4 py-2 rounded"
|
||||
onClick={() => setShowDialog(false)}
|
||||
>
|
||||
{de.cancel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ButtonComponent>
|
||||
<ButtonComponent onClick={() => setShowDialog(false)}>{de.cancel}</ButtonComponent>
|
||||
</div>
|
||||
)}
|
||||
{renameDialog.open && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
||||
<div className="bg-white p-6 rounded shadow">
|
||||
<h3 className="mb-2 font-bold">{de.renameGroup}</h3>
|
||||
<select
|
||||
className="border p-2 mb-2 w-full"
|
||||
value={renameDialog.oldName}
|
||||
onChange={e =>
|
||||
setRenameDialog({
|
||||
...renameDialog,
|
||||
oldName: e.target.value,
|
||||
newName: e.target.value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">{de.selectGroup}</option>
|
||||
{groups
|
||||
.filter(g => g.headerText !== 'Nicht zugeordnet')
|
||||
.map(g => (
|
||||
<option key={g.keyField} value={g.headerText}>
|
||||
{g.headerText}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
className="border p-2 mb-2 w-full"
|
||||
value={renameDialog.newName}
|
||||
onChange={e => setRenameDialog({ ...renameDialog, newName: e.target.value })}
|
||||
placeholder={de.newName}
|
||||
<div className="mt-2">
|
||||
<TextBoxComponent
|
||||
value={newGroupName}
|
||||
placeholder="Raumname"
|
||||
floatLabelType="Auto"
|
||||
change={(args: TextBoxChangedArgs) => setNewGroupName(String(args.value ?? ''))}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded"
|
||||
</div>
|
||||
</DialogComponent>
|
||||
)}
|
||||
{renameDialog.open && (
|
||||
<DialogComponent
|
||||
visible={renameDialog.open}
|
||||
header={de.renameGroup}
|
||||
showCloseIcon={true}
|
||||
close={() => setRenameDialog({ open: false, oldName: '', newName: '' })}
|
||||
target="#dialog-target"
|
||||
width="480px"
|
||||
footerTemplate={() => (
|
||||
<div className="flex gap-2 justify-end">
|
||||
<ButtonComponent
|
||||
cssClass="e-primary"
|
||||
onClick={handleRenameGroup}
|
||||
disabled={!renameDialog.oldName || !renameDialog.newName}
|
||||
>
|
||||
{de.rename}
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-300 px-4 py-2 rounded"
|
||||
onClick={() => setRenameDialog({ open: false, oldName: '', newName: '' })}
|
||||
>
|
||||
</ButtonComponent>
|
||||
<ButtonComponent onClick={() => setRenameDialog({ open: false, oldName: '', newName: '' })}>
|
||||
{de.cancel}
|
||||
</button>
|
||||
</div>
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-3 mt-2">
|
||||
<DropDownListComponent
|
||||
placeholder={de.selectGroup}
|
||||
dataSource={groups.filter(g => g.headerText !== 'Nicht zugeordnet').map(g => g.headerText)}
|
||||
value={renameDialog.oldName}
|
||||
change={(e: DropDownChangeArgs) =>
|
||||
setRenameDialog({
|
||||
...renameDialog,
|
||||
oldName: String(e.value ?? ''),
|
||||
newName: String(e.value ?? ''),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<TextBoxComponent
|
||||
placeholder={de.newName}
|
||||
value={renameDialog.newName}
|
||||
floatLabelType="Auto"
|
||||
change={(args: TextBoxChangedArgs) =>
|
||||
setRenameDialog({ ...renameDialog, newName: String(args.value ?? '') })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</DialogComponent>
|
||||
)}
|
||||
{deleteDialog.open && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
||||
<div className="bg-white p-6 rounded shadow">
|
||||
<h3 className="mb-2 font-bold">{de.deleteGroup}</h3>
|
||||
<select
|
||||
className="border p-2 mb-2 w-full"
|
||||
value={deleteDialog.groupName}
|
||||
onChange={e => setDeleteDialog({ ...deleteDialog, groupName: e.target.value })}
|
||||
>
|
||||
<option value="">{de.selectGroup}</option>
|
||||
{groups
|
||||
.filter(g => g.headerText !== 'Nicht zugeordnet')
|
||||
.map(g => (
|
||||
<option key={g.keyField} value={g.headerText}>
|
||||
{g.headerText}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p>{de.clientsMoved}</p>
|
||||
{deleteDialog.groupName && (
|
||||
<div className="bg-yellow-100 text-yellow-800 p-2 rounded mb-2 text-sm">
|
||||
<strong>{de.warning}</strong> Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b>{' '}
|
||||
wirklich löschen?
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
className="bg-red-500 text-white px-4 py-2 rounded"
|
||||
<DialogComponent
|
||||
visible={deleteDialog.open}
|
||||
header={de.deleteGroup}
|
||||
showCloseIcon={true}
|
||||
close={() => setDeleteDialog({ open: false, groupName: '' })}
|
||||
target="#dialog-target"
|
||||
width="520px"
|
||||
footerTemplate={() => (
|
||||
<div className="flex gap-2 justify-end">
|
||||
<ButtonComponent
|
||||
cssClass="e-danger"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={!deleteDialog.groupName}
|
||||
>
|
||||
{de.deleteGroup}
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-300 px-4 py-2 rounded"
|
||||
onClick={() => setDeleteDialog({ open: false, groupName: '' })}
|
||||
>
|
||||
</ButtonComponent>
|
||||
<ButtonComponent onClick={() => setDeleteDialog({ open: false, groupName: '' })}>
|
||||
{de.cancel}
|
||||
</button>
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-3 mt-2">
|
||||
<DropDownListComponent
|
||||
placeholder={de.selectGroup}
|
||||
dataSource={groups.filter(g => g.headerText !== 'Nicht zugeordnet').map(g => g.headerText)}
|
||||
value={deleteDialog.groupName}
|
||||
change={(e: DropDownChangeArgs) =>
|
||||
setDeleteDialog({ ...deleteDialog, groupName: String(e.value ?? '') })
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-gray-600">{de.clientsMoved}</p>
|
||||
{deleteDialog.groupName && (
|
||||
<div className="bg-yellow-100 text-yellow-800 p-2 rounded text-sm">
|
||||
<strong>{de.warning}</strong> Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b> wirklich löschen?
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogComponent>
|
||||
)}
|
||||
{showDeleteConfirm && deleteDialog.groupName && (
|
||||
<DialogComponent
|
||||
width="350px"
|
||||
width="380px"
|
||||
header={de.confirmDelete}
|
||||
visible={showDeleteConfirm}
|
||||
showCloseIcon={true}
|
||||
close={() => setShowDeleteConfirm(false)}
|
||||
target="#dialog-target"
|
||||
footerTemplate={() => (
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button
|
||||
className="bg-red-500 text-white px-4 py-2 rounded"
|
||||
<ButtonComponent
|
||||
cssClass="e-danger"
|
||||
onClick={() => {
|
||||
handleDeleteGroup(deleteDialog.groupName);
|
||||
handleDeleteGroup(deleteDialog.groupName!);
|
||||
setShowDeleteConfirm(false);
|
||||
}}
|
||||
>
|
||||
{de.yesDelete}
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-300 px-4 py-2 rounded"
|
||||
</ButtonComponent>
|
||||
<ButtonComponent
|
||||
onClick={() => {
|
||||
setShowDeleteConfirm(false);
|
||||
setDeleteDialog({ open: false, groupName: '' });
|
||||
}}
|
||||
>
|
||||
{de.cancel}
|
||||
</button>
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
@@ -511,8 +521,6 @@ const Infoscreen_groups: React.FC = () => {
|
||||
</DialogComponent>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,20 @@ import { registerLicense } from '@syncfusion/ej2-base';
|
||||
import '@syncfusion/ej2-base/styles/material3.css';
|
||||
import '@syncfusion/ej2-navigations/styles/material3.css';
|
||||
import '@syncfusion/ej2-buttons/styles/material3.css';
|
||||
import '@syncfusion/ej2-inputs/styles/material3.css';
|
||||
import '@syncfusion/ej2-dropdowns/styles/material3.css';
|
||||
import '@syncfusion/ej2-popups/styles/material3.css';
|
||||
import '@syncfusion/ej2-kanban/styles/material3.css';
|
||||
// Additional components used across the app
|
||||
import '@syncfusion/ej2-grids/styles/material3.css';
|
||||
import '@syncfusion/ej2-react-schedule/styles/material3.css';
|
||||
import '@syncfusion/ej2-react-filemanager/styles/material3.css';
|
||||
import '@syncfusion/ej2-notifications/styles/material3.css';
|
||||
import '@syncfusion/ej2-layouts/styles/material3.css';
|
||||
import '@syncfusion/ej2-lists/styles/material3.css';
|
||||
import '@syncfusion/ej2-calendars/styles/material3.css';
|
||||
import '@syncfusion/ej2-splitbuttons/styles/material3.css';
|
||||
import '@syncfusion/ej2-icons/styles/material3.css';
|
||||
|
||||
// Setze hier deinen Lizenzschlüssel ein
|
||||
registerLicense(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { PagerComponent } from '@syncfusion/ej2-react-grids';
|
||||
|
||||
interface ProgramInfo {
|
||||
appName: string;
|
||||
@@ -6,9 +7,7 @@ interface ProgramInfo {
|
||||
copyright: string;
|
||||
supportContact: string;
|
||||
description: string;
|
||||
techStack: {
|
||||
[key: string]: string;
|
||||
};
|
||||
techStack: Record<string, string>;
|
||||
openSourceComponents: {
|
||||
frontend: { name: string; license: string }[];
|
||||
backend: { name: string; license: string }[];
|
||||
@@ -27,26 +26,32 @@ interface ProgramInfo {
|
||||
const Programminfo: React.FC = () => {
|
||||
const [info, setInfo] = useState<ProgramInfo | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const pageSize = 5;
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
fetch('/program-info.json')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Netzwerk-Antwort war nicht ok');
|
||||
}
|
||||
return response.json();
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Netzwerk-Antwort war nicht ok');
|
||||
return res.json();
|
||||
})
|
||||
.then(data => setInfo(data))
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Programminformationen:', error);
|
||||
setError('Informationen konnten nicht geladen werden.');
|
||||
.then((data: ProgramInfo) => {
|
||||
if (isMounted) setInfo(data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Fehler beim Laden der Programminformationen:', err);
|
||||
if (isMounted) setError('Informationen konnten nicht geladen werden.');
|
||||
});
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-4 text-red-600">Fehler</h2>
|
||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 700, marginBottom: '1rem', color: '#dc2626' }}>Fehler</h2>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -55,24 +60,32 @@ const Programminfo: React.FC = () => {
|
||||
if (!info) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-4">Programminfo</h2>
|
||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 700, marginBottom: '1rem' }}>Programminfo</h2>
|
||||
<p>Lade Informationen...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const monoFont = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">{info.appName}</h2>
|
||||
<p className="text-gray-600">{info.description}</p>
|
||||
<h2 style={{ fontSize: '1.75rem', fontWeight: 700, marginBottom: '0.5rem' }}>{info.appName}</h2>
|
||||
<p style={{ color: '#4b5563' }}>{info.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '2rem' }}>
|
||||
{/* Allgemeine Infos & Build */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h3 className="text-xl font-semibold mb-4 border-b pb-2">Allgemein</h3>
|
||||
<div className="space-y-3">
|
||||
<div style={{ flex: '1 1 360px', minWidth: '320px' }}>
|
||||
<div className="e-card">
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-title">Allgemein</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
<p>
|
||||
<strong>Version:</strong> {info.version}
|
||||
</p>
|
||||
@@ -81,68 +94,116 @@ const Programminfo: React.FC = () => {
|
||||
</p>
|
||||
<p>
|
||||
<strong>Support:</strong>{' '}
|
||||
<a href={`mailto:${info.supportContact}`} className="text-blue-600 hover:underline">
|
||||
<a href={`mailto:${info.supportContact}`} style={{ color: '#2563eb', textDecoration: 'none' }}>
|
||||
{info.supportContact}
|
||||
</a>
|
||||
</p>
|
||||
<hr className="my-4" />
|
||||
<h4 className="font-semibold">Build-Informationen</h4>
|
||||
<hr style={{ margin: '1rem 0' }} />
|
||||
<h4 style={{ fontWeight: 600 }}>Build-Informationen</h4>
|
||||
<p>
|
||||
<strong>Build-Datum:</strong>{' '}
|
||||
{new Date(info.buildInfo.buildDate).toLocaleString('de-DE')}
|
||||
<strong>Build-Datum:</strong> {new Date(info.buildInfo.buildDate).toLocaleString('de-DE')}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Commit-ID:</strong>{' '}
|
||||
<span className="font-mono text-sm bg-gray-100 p-1 rounded">
|
||||
<span style={{ fontFamily: monoFont, fontSize: '0.875rem', background: '#f3f4f6', padding: '0.125rem 0.25rem', borderRadius: '0.25rem' }}>
|
||||
{info.buildInfo.commitId}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Technischer Stack */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h3 className="text-xl font-semibold mb-4 border-b pb-2">Technologie-Stack</h3>
|
||||
<ul className="list-disc list-inside space-y-2">
|
||||
<div style={{ flex: '1 1 360px', minWidth: '320px' }}>
|
||||
<div className="e-card">
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-title">Technologie-Stack</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content">
|
||||
<ul style={{ paddingLeft: '1rem', margin: 0, display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
{Object.entries(info.techStack).map(([key, value]) => (
|
||||
<li key={key}>
|
||||
<span className="font-semibold capitalize">{key}:</span> {value}
|
||||
<span style={{ fontWeight: 600, textTransform: 'capitalize' }}>{key}:</span> {value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Changelog */}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-4">Änderungsprotokoll (Changelog)</h3>
|
||||
<div className="space-y-6">
|
||||
{info.changelog.map(log => (
|
||||
<div key={log.version} className="bg-white p-6 rounded-lg shadow">
|
||||
<h4 className="font-bold text-lg mb-2">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '0.5rem' }}>
|
||||
<h3 style={{ fontSize: '1.25rem', fontWeight: 600, margin: 0 }}>Änderungsprotokoll (Changelog)</h3>
|
||||
<div style={{ marginLeft: 'auto' }}>
|
||||
<span style={{ color: '#6b7280', fontSize: '0.875rem' }}>
|
||||
Insgesamt {info.changelog.length} Einträge
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: '0.75rem' }}>
|
||||
<PagerComponent
|
||||
totalRecordsCount={info.changelog.length}
|
||||
pageSize={pageSize}
|
||||
pageCount={5}
|
||||
currentPage={currentPage}
|
||||
click={(args: { currentPage: number }) => setCurrentPage(args.currentPage)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
{info.changelog
|
||||
.slice((currentPage - 1) * pageSize, (currentPage - 1) * pageSize + pageSize)
|
||||
.map(log => (
|
||||
<div key={log.version} className="e-card">
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-title">
|
||||
Version {log.version}{' '}
|
||||
<span className="text-sm font-normal text-gray-500">
|
||||
<span style={{ fontSize: '0.875rem', fontWeight: 400, color: '#6b7280' }}>
|
||||
- {new Date(log.date).toLocaleDateString('de-DE')}
|
||||
</span>
|
||||
</h4>
|
||||
<ul className="list-disc list-inside space-y-1 text-gray-700">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content">
|
||||
<ul style={{ color: '#374151', paddingLeft: '1rem', margin: 0, display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
|
||||
{log.changes.map((change, index) => (
|
||||
<li key={index}>{change}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: '0.75rem' }}>
|
||||
<PagerComponent
|
||||
totalRecordsCount={info.changelog.length}
|
||||
pageSize={pageSize}
|
||||
pageCount={5}
|
||||
currentPage={currentPage}
|
||||
click={(args: { currentPage: number }) => setCurrentPage(args.currentPage)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Open Source Komponenten */}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-4">Verwendete Open-Source-Komponenten</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<h3 style={{ fontSize: '1.25rem', fontWeight: 600, marginBottom: '1rem' }}>Verwendete Open-Source-Komponenten</h3>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '2rem' }}>
|
||||
{info.openSourceComponents.frontend && (
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h4 className="font-bold mb-3">Frontend</h4>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<div style={{ flex: '1 1 360px', minWidth: '320px' }}>
|
||||
<div className="e-card">
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-title">Frontend</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content">
|
||||
<ul style={{ paddingLeft: '1rem', margin: 0, display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
|
||||
{info.openSourceComponents.frontend.map(item => (
|
||||
<li key={item.name}>
|
||||
{item.name} ({item.license}-Lizenz)
|
||||
@@ -150,11 +211,19 @@ const Programminfo: React.FC = () => {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{info.openSourceComponents.backend && (
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h4 className="font-bold mb-3">Backend</h4>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<div style={{ flex: '1 1 360px', minWidth: '320px' }}>
|
||||
<div className="e-card">
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-title">Backend</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content">
|
||||
<ul style={{ paddingLeft: '1rem', margin: 0, display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
|
||||
{info.openSourceComponents.backend.map(item => (
|
||||
<li key={item.name}>
|
||||
{item.name} ({item.license}-Lizenz)
|
||||
@@ -162,6 +231,8 @@ const Programminfo: React.FC = () => {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user