Toast-feedback feature for dashboard
This commit is contained in:
12
dashboard/package-lock.json
generated
12
dashboard/package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"@syncfusion/ej2-react-calendars": "^30.1.37",
|
"@syncfusion/ej2-react-calendars": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-grids": "^30.1.37",
|
"@syncfusion/ej2-react-grids": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-kanban": "^30.1.37",
|
"@syncfusion/ej2-react-kanban": "^30.1.37",
|
||||||
|
"@syncfusion/ej2-react-notifications": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-popups": "^30.1.37",
|
"@syncfusion/ej2-react-popups": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-schedule": "^30.1.37",
|
"@syncfusion/ej2-react-schedule": "^30.1.37",
|
||||||
"cldr-data": "^36.0.4",
|
"cldr-data": "^36.0.4",
|
||||||
@@ -1237,6 +1238,17 @@
|
|||||||
"@syncfusion/ej2-react-base": "~30.1.37"
|
"@syncfusion/ej2-react-base": "~30.1.37"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@syncfusion/ej2-react-notifications": {
|
||||||
|
"version": "30.1.37",
|
||||||
|
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-react-notifications/-/ej2-react-notifications-30.1.37.tgz",
|
||||||
|
"integrity": "sha512-jNeoj/vV7Cie3eWYPCrtSvMADcgSMAOKm6ZxwgMogUHoB8tG94HgOTWGSDNEetf+f9deFKvwqgtaYGbJW/gAIg==",
|
||||||
|
"license": "SEE LICENSE IN license",
|
||||||
|
"dependencies": {
|
||||||
|
"@syncfusion/ej2-base": "~30.1.37",
|
||||||
|
"@syncfusion/ej2-notifications": "30.1.37",
|
||||||
|
"@syncfusion/ej2-react-base": "~30.1.37"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@syncfusion/ej2-react-popups": {
|
"node_modules/@syncfusion/ej2-react-popups": {
|
||||||
"version": "30.1.37",
|
"version": "30.1.37",
|
||||||
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-react-popups/-/ej2-react-popups-30.1.37.tgz",
|
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-react-popups/-/ej2-react-popups-30.1.37.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@syncfusion/ej2-react-calendars": "^30.1.37",
|
"@syncfusion/ej2-react-calendars": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-grids": "^30.1.37",
|
"@syncfusion/ej2-react-grids": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-kanban": "^30.1.37",
|
"@syncfusion/ej2-react-kanban": "^30.1.37",
|
||||||
|
"@syncfusion/ej2-react-notifications": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-popups": "^30.1.37",
|
"@syncfusion/ej2-react-popups": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-schedule": "^30.1.37",
|
"@syncfusion/ej2-react-schedule": "^30.1.37",
|
||||||
"cldr-data": "^36.0.4",
|
"cldr-data": "^36.0.4",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
@import "../node_modules/@syncfusion/ej2-splitbuttons/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-react-schedule/styles/material.css";
|
||||||
@import "../node_modules/@syncfusion/ej2-kanban/styles/material.css";
|
@import "../node_modules/@syncfusion/ej2-kanban/styles/material.css";
|
||||||
|
@import "../node_modules/@syncfusion/ej2-notifications/styles/material.css";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Inter, 'Segoe UI', Roboto, Arial, sans-serif;
|
font-family: Inter, 'Segoe UI', Roboto, Arial, sans-serif;
|
||||||
@@ -96,7 +97,7 @@ body {
|
|||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header-Text noch spezifischer und mit !important */
|
/* Header-Text noch spezifischer and mit !important */
|
||||||
.e-kanban .e-kanban-table .e-header-cells .e-header-text {
|
.e-kanban .e-kanban-table .e-header-cells .e-header-text {
|
||||||
color: color-mix(in srgb, var(--sidebar-fg) 85%, #000 15%) !important;
|
color: color-mix(in srgb, var(--sidebar-fg) 85%, #000 15%) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
MonitorDotIcon,
|
MonitorDotIcon,
|
||||||
LogOut,
|
LogOut,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { ToastProvider } from './components/ToastProvider';
|
||||||
|
|
||||||
const sidebarItems = [
|
const sidebarItems = [
|
||||||
{ name: 'Dashboard', path: '/', icon: LayoutDashboard },
|
{ name: 'Dashboard', path: '/', icon: LayoutDashboard },
|
||||||
@@ -120,18 +121,20 @@ const Layout: React.FC = () => {
|
|||||||
|
|
||||||
const App: React.FC = () => (
|
const App: React.FC = () => (
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<ToastProvider>
|
||||||
<Route path="/" element={<Layout />}>
|
<Routes>
|
||||||
<Route index element={<Dashboard />} />
|
<Route path="/" element={<Layout />}>
|
||||||
<Route path="termine" element={<Appointments />} />
|
<Route index element={<Dashboard />} />
|
||||||
<Route path="ressourcen" element={<Ressourcen />} />
|
<Route path="termine" element={<Appointments />} />
|
||||||
<Route path="Infoscreens" element={<Infoscreens />} />
|
<Route path="ressourcen" element={<Ressourcen />} />
|
||||||
<Route path="infoscr_groups" element={<Infoscreen_groups />} />
|
<Route path="Infoscreens" element={<Infoscreens />} />
|
||||||
<Route path="medien" element={<Medien />} />
|
<Route path="infoscr_groups" element={<Infoscreen_groups />} />
|
||||||
<Route path="benutzer" element={<Benutzer />} />
|
<Route path="medien" element={<Medien />} />
|
||||||
<Route path="einstellungen" element={<Einstellungen />} />
|
<Route path="benutzer" element={<Benutzer />} />
|
||||||
</Route>
|
<Route path="einstellungen" element={<Einstellungen />} />
|
||||||
</Routes>
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</ToastProvider>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,28 +4,36 @@ export async function createGroup(name: string) {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name }),
|
body: JSON.stringify({ name }),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Fehler beim Erstellen der Gruppe');
|
const data = await res.json();
|
||||||
return await res.json();
|
if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Erstellen der Gruppe');
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchGroups() {
|
export async function fetchGroups() {
|
||||||
const res = await fetch('/api/groups');
|
const res = await fetch('/api/groups');
|
||||||
if (!res.ok) throw new Error('Fehler beim Laden der Gruppen');
|
const data = await res.json();
|
||||||
return await res.json();
|
if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Laden der Gruppen');
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteGroup(groupName: string) {
|
export async function deleteGroup(groupName: string) {
|
||||||
// Passe ggf. an deine API an
|
const res = await fetch(`/api/groups/byname/${encodeURIComponent(groupName)}`, {
|
||||||
return fetch(`/api/groups/byname/${encodeURIComponent(groupName)}`, { method: 'DELETE' });
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Löschen der Gruppe');
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renameGroup(oldName: string, newName: string) {
|
export async function renameGroup(oldName: string, newName: string) {
|
||||||
// Passe ggf. an deine API an
|
const res = await fetch(`/api/groups/byname/${encodeURIComponent(oldName)}`, {
|
||||||
return fetch(`/api/groups/byname/${encodeURIComponent(oldName)}`, {
|
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ newName }),
|
body: JSON.stringify({ newName: newName }),
|
||||||
});
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Umbenennen der Gruppe');
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hier kannst du später weitere Funktionen ergänzen:
|
// Hier kannst du später weitere Funktionen ergänzen:
|
||||||
|
|||||||
24
dashboard/src/components/ToastProvider.tsx
Normal file
24
dashboard/src/components/ToastProvider.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React, { createContext, useRef, useContext } from 'react';
|
||||||
|
import { ToastComponent, type ToastModel } from '@syncfusion/ej2-react-notifications';
|
||||||
|
|
||||||
|
const ToastContext = createContext<{ show: (opts: ToastModel) => void }>({ show: () => {} });
|
||||||
|
|
||||||
|
export const useToast = () => useContext(ToastContext);
|
||||||
|
|
||||||
|
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const toastRef = useRef<ToastComponent>(null);
|
||||||
|
|
||||||
|
const show = (opts: ToastModel) => toastRef.current?.show(opts);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={{ show }}>
|
||||||
|
{children}
|
||||||
|
<ToastComponent
|
||||||
|
ref={toastRef}
|
||||||
|
position={{ X: 'Right', Y: 'Top' }}
|
||||||
|
timeOut={5000} // Standard: 5 Sekunden
|
||||||
|
showCloseButton={false}
|
||||||
|
/>
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,6 +5,8 @@ import { fetchGroups, createGroup, deleteGroup, renameGroup } from './apiGroups'
|
|||||||
import type { Client } from './apiClients';
|
import type { Client } from './apiClients';
|
||||||
import type { KanbanComponent as KanbanComponentType } from '@syncfusion/ej2-react-kanban';
|
import type { KanbanComponent as KanbanComponentType } from '@syncfusion/ej2-react-kanban';
|
||||||
import { DialogComponent } from '@syncfusion/ej2-react-popups';
|
import { DialogComponent } from '@syncfusion/ej2-react-popups';
|
||||||
|
import { useToast } from './components/ToastProvider';
|
||||||
|
import { L10n } from '@syncfusion/ej2-base';
|
||||||
|
|
||||||
interface KanbanClient extends Client {
|
interface KanbanClient extends Client {
|
||||||
Id: string;
|
Id: string;
|
||||||
@@ -25,7 +27,46 @@ interface KanbanDragEventArgs {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const de = {
|
||||||
|
title: 'Gruppen',
|
||||||
|
newGroup: 'Neue Raumgruppe',
|
||||||
|
renameGroup: 'Gruppe umbenennen',
|
||||||
|
deleteGroup: 'Gruppe löschen',
|
||||||
|
add: 'Hinzufügen',
|
||||||
|
cancel: 'Abbrechen',
|
||||||
|
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.',
|
||||||
|
groupCreated: 'Gruppe angelegt',
|
||||||
|
groupDeleted: 'Gruppe gelöscht. Clients in "Nicht zugeordnet" verschoben',
|
||||||
|
groupRenamed: 'Gruppenname geändert',
|
||||||
|
selectGroup: 'Gruppe wählen',
|
||||||
|
newName: 'Neuer Name',
|
||||||
|
warning: 'Achtung:',
|
||||||
|
yesDelete: 'Ja, löschen',
|
||||||
|
};
|
||||||
|
|
||||||
|
L10n.load({
|
||||||
|
de: {
|
||||||
|
kanban: {
|
||||||
|
items: 'Clients',
|
||||||
|
addTitle: 'Neue Karte hinzufügen',
|
||||||
|
editTitle: 'Karte bearbeiten',
|
||||||
|
deleteTitle: 'Karte löschen',
|
||||||
|
edit: 'Bearbeiten',
|
||||||
|
delete: 'Löschen',
|
||||||
|
save: 'Speichern',
|
||||||
|
cancel: 'Abbrechen',
|
||||||
|
yes: 'Ja',
|
||||||
|
no: 'Nein',
|
||||||
|
noCard: 'Keine Clients vorhanden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const Infoscreen_groups: React.FC = () => {
|
const Infoscreen_groups: React.FC = () => {
|
||||||
|
const toast = useToast();
|
||||||
const [clients, setClients] = useState<KanbanClient[]>([]);
|
const [clients, setClients] = useState<KanbanClient[]>([]);
|
||||||
const [groups, setGroups] = useState<{ keyField: string; headerText: string }[]>([]);
|
const [groups, setGroups] = useState<{ keyField: string; headerText: string }[]>([]);
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
@@ -74,11 +115,22 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
if (!newGroupName.trim()) return;
|
if (!newGroupName.trim()) return;
|
||||||
try {
|
try {
|
||||||
const newGroup = await createGroup(newGroupName);
|
const newGroup = await createGroup(newGroupName);
|
||||||
|
toast.show({
|
||||||
|
content: de.groupCreated,
|
||||||
|
cssClass: 'e-toast-success',
|
||||||
|
timeOut: 5000,
|
||||||
|
showCloseButton: false,
|
||||||
|
});
|
||||||
setGroups([...groups, { keyField: newGroup.name, headerText: newGroup.name }]);
|
setGroups([...groups, { keyField: newGroup.name, headerText: newGroup.name }]);
|
||||||
setNewGroupName('');
|
setNewGroupName('');
|
||||||
setShowDialog(false);
|
setShowDialog(false);
|
||||||
} catch {
|
} catch (err) {
|
||||||
alert('Fehler beim Erstellen der Gruppe');
|
toast.show({
|
||||||
|
content: (err as Error).message,
|
||||||
|
cssClass: 'e-toast-danger',
|
||||||
|
timeOut: 0,
|
||||||
|
showCloseButton: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,6 +146,12 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
await deleteGroup(groupName);
|
await deleteGroup(groupName);
|
||||||
|
toast.show({
|
||||||
|
content: de.groupDeleted,
|
||||||
|
cssClass: 'e-toast-success',
|
||||||
|
timeOut: 5000,
|
||||||
|
showCloseButton: false,
|
||||||
|
});
|
||||||
// Gruppen und Clients neu laden
|
// Gruppen und Clients neu laden
|
||||||
const groupData = await fetchGroups();
|
const groupData = await fetchGroups();
|
||||||
const groupMap = Object.fromEntries(groupData.map((g: Group) => [g.id, g.name]));
|
const groupMap = Object.fromEntries(groupData.map((g: Group) => [g.id, g.name]));
|
||||||
@@ -107,8 +165,13 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
Summary: c.location || `Client ${i + 1}`,
|
Summary: c.location || `Client ${i + 1}`,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
} catch {
|
} catch (err) {
|
||||||
alert('Fehler beim Löschen der Gruppe');
|
toast.show({
|
||||||
|
content: (err as Error).message,
|
||||||
|
cssClass: 'e-toast-danger',
|
||||||
|
timeOut: 0,
|
||||||
|
showCloseButton: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setDeleteDialog({ open: false, groupName: '' });
|
setDeleteDialog({ open: false, groupName: '' });
|
||||||
};
|
};
|
||||||
@@ -117,6 +180,12 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
const handleRenameGroup = async () => {
|
const handleRenameGroup = async () => {
|
||||||
try {
|
try {
|
||||||
await renameGroup(renameDialog.oldName, renameDialog.newName);
|
await renameGroup(renameDialog.oldName, renameDialog.newName);
|
||||||
|
toast.show({
|
||||||
|
content: de.groupRenamed,
|
||||||
|
cssClass: 'e-toast-success',
|
||||||
|
timeOut: 5000,
|
||||||
|
showCloseButton: false,
|
||||||
|
});
|
||||||
// Gruppen und Clients neu laden
|
// Gruppen und Clients neu laden
|
||||||
const groupData = await fetchGroups();
|
const groupData = await fetchGroups();
|
||||||
const groupMap = Object.fromEntries(groupData.map((g: Group) => [g.id, g.name]));
|
const groupMap = Object.fromEntries(groupData.map((g: Group) => [g.id, g.name]));
|
||||||
@@ -130,8 +199,13 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
Summary: c.location || `Client ${i + 1}`,
|
Summary: c.location || `Client ${i + 1}`,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
} catch {
|
} catch (err) {
|
||||||
alert('Fehler beim Umbenennen der Gruppe');
|
toast.show({
|
||||||
|
content: (err as Error).message,
|
||||||
|
cssClass: 'e-toast-danger',
|
||||||
|
timeOut: 0,
|
||||||
|
showCloseButton: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setRenameDialog({ open: false, oldName: '', newName: '' });
|
setRenameDialog({ open: false, oldName: '', newName: '' });
|
||||||
};
|
};
|
||||||
@@ -217,28 +291,29 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="dialog-target">
|
<div id="dialog-target">
|
||||||
<h2 className="text-xl font-bold mb-4">Gruppen</h2>
|
<h2 className="text-xl font-bold mb-4">{de.title}</h2>
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-4">
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 bg-blue-500 text-white rounded"
|
className="px-4 py-2 bg-blue-500 text-white rounded"
|
||||||
onClick={() => setShowDialog(true)}
|
onClick={() => setShowDialog(true)}
|
||||||
>
|
>
|
||||||
Neue Raumgruppe
|
{de.newGroup}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 bg-yellow-500 text-white rounded"
|
className="px-4 py-2 bg-yellow-500 text-white rounded"
|
||||||
onClick={() => setRenameDialog({ open: true, oldName: '', newName: '' })}
|
onClick={() => setRenameDialog({ open: true, oldName: '', newName: '' })}
|
||||||
>
|
>
|
||||||
Gruppe umbenennen
|
{de.renameGroup}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 bg-red-500 text-white rounded"
|
className="px-4 py-2 bg-red-500 text-white rounded"
|
||||||
onClick={() => setDeleteDialog({ open: true, groupName: '' })}
|
onClick={() => setDeleteDialog({ open: true, groupName: '' })}
|
||||||
>
|
>
|
||||||
Gruppe löschen
|
{de.deleteGroup}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<KanbanComponent
|
<KanbanComponent
|
||||||
|
locale="de"
|
||||||
id="kanban"
|
id="kanban"
|
||||||
keyField="Status"
|
keyField="Status"
|
||||||
dataSource={clients}
|
dataSource={clients}
|
||||||
@@ -255,7 +330,7 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
{showDialog && (
|
{showDialog && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
||||||
<div className="bg-white p-6 rounded shadow">
|
<div className="bg-white p-6 rounded shadow">
|
||||||
<h3 className="mb-2 font-bold">Neue Raumgruppe</h3>
|
<h3 className="mb-2 font-bold">{de.newGroup}</h3>
|
||||||
<input
|
<input
|
||||||
className="border p-2 mb-2 w-full"
|
className="border p-2 mb-2 w-full"
|
||||||
value={newGroupName}
|
value={newGroupName}
|
||||||
@@ -264,13 +339,13 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button className="bg-blue-500 text-white px-4 py-2 rounded" onClick={handleAddGroup}>
|
<button className="bg-blue-500 text-white px-4 py-2 rounded" onClick={handleAddGroup}>
|
||||||
Hinzufügen
|
{de.add}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="bg-gray-300 px-4 py-2 rounded"
|
className="bg-gray-300 px-4 py-2 rounded"
|
||||||
onClick={() => setShowDialog(false)}
|
onClick={() => setShowDialog(false)}
|
||||||
>
|
>
|
||||||
Abbrechen
|
{de.cancel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -279,7 +354,7 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
{renameDialog.open && (
|
{renameDialog.open && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
||||||
<div className="bg-white p-6 rounded shadow">
|
<div className="bg-white p-6 rounded shadow">
|
||||||
<h3 className="mb-2 font-bold">Raumgruppe umbenennen</h3>
|
<h3 className="mb-2 font-bold">{de.renameGroup}</h3>
|
||||||
<select
|
<select
|
||||||
className="border p-2 mb-2 w-full"
|
className="border p-2 mb-2 w-full"
|
||||||
value={renameDialog.oldName}
|
value={renameDialog.oldName}
|
||||||
@@ -291,7 +366,7 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<option value="">Gruppe wählen</option>
|
<option value="">{de.selectGroup}</option>
|
||||||
{groups
|
{groups
|
||||||
.filter(g => g.headerText !== 'Nicht zugeordnet')
|
.filter(g => g.headerText !== 'Nicht zugeordnet')
|
||||||
.map(g => (
|
.map(g => (
|
||||||
@@ -304,7 +379,7 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
className="border p-2 mb-2 w-full"
|
className="border p-2 mb-2 w-full"
|
||||||
value={renameDialog.newName}
|
value={renameDialog.newName}
|
||||||
onChange={e => setRenameDialog({ ...renameDialog, newName: e.target.value })}
|
onChange={e => setRenameDialog({ ...renameDialog, newName: e.target.value })}
|
||||||
placeholder="Neuer Name"
|
placeholder={de.newName}
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -312,13 +387,13 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
onClick={handleRenameGroup}
|
onClick={handleRenameGroup}
|
||||||
disabled={!renameDialog.oldName || !renameDialog.newName}
|
disabled={!renameDialog.oldName || !renameDialog.newName}
|
||||||
>
|
>
|
||||||
Umbenennen
|
{de.rename}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="bg-gray-300 px-4 py-2 rounded"
|
className="bg-gray-300 px-4 py-2 rounded"
|
||||||
onClick={() => setRenameDialog({ open: false, oldName: '', newName: '' })}
|
onClick={() => setRenameDialog({ open: false, oldName: '', newName: '' })}
|
||||||
>
|
>
|
||||||
Abbrechen
|
{de.cancel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -327,13 +402,13 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
{deleteDialog.open && (
|
{deleteDialog.open && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
||||||
<div className="bg-white p-6 rounded shadow">
|
<div className="bg-white p-6 rounded shadow">
|
||||||
<h3 className="mb-2 font-bold">Gruppe löschen</h3>
|
<h3 className="mb-2 font-bold">{de.deleteGroup}</h3>
|
||||||
<select
|
<select
|
||||||
className="border p-2 mb-2 w-full"
|
className="border p-2 mb-2 w-full"
|
||||||
value={deleteDialog.groupName}
|
value={deleteDialog.groupName}
|
||||||
onChange={e => setDeleteDialog({ ...deleteDialog, groupName: e.target.value })}
|
onChange={e => setDeleteDialog({ ...deleteDialog, groupName: e.target.value })}
|
||||||
>
|
>
|
||||||
<option value="">Gruppe wählen</option>
|
<option value="">{de.selectGroup}</option>
|
||||||
{groups
|
{groups
|
||||||
.filter(g => g.headerText !== 'Nicht zugeordnet')
|
.filter(g => g.headerText !== 'Nicht zugeordnet')
|
||||||
.map(g => (
|
.map(g => (
|
||||||
@@ -342,10 +417,10 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<p>Alle Clients werden in "Nicht zugeordnet" verschoben.</p>
|
<p>{de.clientsMoved}</p>
|
||||||
{deleteDialog.groupName && (
|
{deleteDialog.groupName && (
|
||||||
<div className="bg-yellow-100 text-yellow-800 p-2 rounded mb-2 text-sm">
|
<div className="bg-yellow-100 text-yellow-800 p-2 rounded mb-2 text-sm">
|
||||||
<strong>Achtung:</strong> Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b>{' '}
|
<strong>{de.warning}</strong> Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b>{' '}
|
||||||
wirklich löschen?
|
wirklich löschen?
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -355,20 +430,20 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
onClick={() => setShowDeleteConfirm(true)}
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
disabled={!deleteDialog.groupName}
|
disabled={!deleteDialog.groupName}
|
||||||
>
|
>
|
||||||
Löschen
|
{de.deleteGroup}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="bg-gray-300 px-4 py-2 rounded"
|
className="bg-gray-300 px-4 py-2 rounded"
|
||||||
onClick={() => setDeleteDialog({ open: false, groupName: '' })}
|
onClick={() => setDeleteDialog({ open: false, groupName: '' })}
|
||||||
>
|
>
|
||||||
Abbrechen
|
{de.cancel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showDeleteConfirm && deleteDialog.groupName && (
|
{showDeleteConfirm && deleteDialog.groupName && (
|
||||||
<DialogComponent
|
<DialogComponent
|
||||||
width="350px"
|
width="350px"
|
||||||
header="Löschbestätigung"
|
header={de.confirmDelete}
|
||||||
visible={showDeleteConfirm}
|
visible={showDeleteConfirm}
|
||||||
close={() => setShowDeleteConfirm(false)}
|
close={() => setShowDeleteConfirm(false)}
|
||||||
footerTemplate={() => (
|
footerTemplate={() => (
|
||||||
@@ -380,7 +455,7 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
setShowDeleteConfirm(false);
|
setShowDeleteConfirm(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Ja, löschen
|
{de.yesDelete}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="bg-gray-300 px-4 py-2 rounded"
|
className="bg-gray-300 px-4 py-2 rounded"
|
||||||
@@ -389,7 +464,7 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
setDeleteDialog({ open: false, groupName: '' });
|
setDeleteDialog({ open: false, groupName: '' });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Abbrechen
|
{de.cancel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -397,9 +472,7 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b> wirklich löschen?
|
Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b> wirklich löschen?
|
||||||
<br />
|
<br />
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">{de.clientsMoved}</span>
|
||||||
Alle Clients werden in "Nicht zugeordnet" verschoben.
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogComponent>
|
</DialogComponent>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from database import Session
|
from database import Session
|
||||||
from models import ClientGroup
|
from models import ClientGroup
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
|
from sqlalchemy import func
|
||||||
import sys
|
import sys
|
||||||
sys.path.append('/workspace')
|
sys.path.append('/workspace')
|
||||||
|
|
||||||
@@ -112,9 +113,9 @@ def rename_group_by_name(old_name):
|
|||||||
return jsonify({"error": "Gruppe nicht gefunden"}), 404
|
return jsonify({"error": "Gruppe nicht gefunden"}), 404
|
||||||
|
|
||||||
# Prüfe, ob der neue Name schon existiert
|
# Prüfe, ob der neue Name schon existiert
|
||||||
if session.query(ClientGroup).filter_by(name=new_name).first():
|
if session.query(ClientGroup).filter(func.binary(ClientGroup.name) == new_name).first():
|
||||||
session.close()
|
session.close()
|
||||||
return jsonify({"error": "Gruppe mit diesem Namen existiert bereits"}), 409
|
return jsonify({"error": f'Gruppe mit dem Namen "{new_name}" existiert bereits', "duplicate_name": new_name}), 409
|
||||||
|
|
||||||
group.name = new_name
|
group.name = new_name
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|||||||
Reference in New Issue
Block a user