Toast-feedback feature for dashboard

This commit is contained in:
2025-06-29 08:25:19 +00:00
parent 6639006d65
commit 7d7204e7c6
8 changed files with 177 additions and 54 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
} }

View File

@@ -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>
); );

View File

@@ -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:

View 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>
);
};

View File

@@ -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>
)} )}

View File

@@ -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()