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-grids": "^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-schedule": "^30.1.37",
|
||||
"cldr-data": "^36.0.4",
|
||||
@@ -1237,6 +1238,17 @@
|
||||
"@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": {
|
||||
"version": "30.1.37",
|
||||
"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-grids": "^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-schedule": "^30.1.37",
|
||||
"cldr-data": "^36.0.4",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
@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";
|
||||
|
||||
body {
|
||||
font-family: Inter, 'Segoe UI', Roboto, Arial, sans-serif;
|
||||
@@ -96,7 +97,7 @@ body {
|
||||
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 {
|
||||
color: color-mix(in srgb, var(--sidebar-fg) 85%, #000 15%) !important;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
MonitorDotIcon,
|
||||
LogOut,
|
||||
} from 'lucide-react';
|
||||
import { ToastProvider } from './components/ToastProvider';
|
||||
|
||||
const sidebarItems = [
|
||||
{ name: 'Dashboard', path: '/', icon: LayoutDashboard },
|
||||
@@ -120,6 +121,7 @@ const Layout: React.FC = () => {
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Router>
|
||||
<ToastProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
@@ -132,6 +134,7 @@ const App: React.FC = () => (
|
||||
<Route path="einstellungen" element={<Einstellungen />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</ToastProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
|
||||
@@ -4,28 +4,36 @@ export async function createGroup(name: string) {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Fehler beim Erstellen der Gruppe');
|
||||
return await res.json();
|
||||
const data = await res.json();
|
||||
if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Erstellen der Gruppe');
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchGroups() {
|
||||
const res = await fetch('/api/groups');
|
||||
if (!res.ok) throw new Error('Fehler beim Laden der Gruppen');
|
||||
return await res.json();
|
||||
const data = 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) {
|
||||
// Passe ggf. an deine API an
|
||||
return fetch(`/api/groups/byname/${encodeURIComponent(groupName)}`, { method: 'DELETE' });
|
||||
const res = await fetch(`/api/groups/byname/${encodeURIComponent(groupName)}`, {
|
||||
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) {
|
||||
// Passe ggf. an deine API an
|
||||
return fetch(`/api/groups/byname/${encodeURIComponent(oldName)}`, {
|
||||
const res = await fetch(`/api/groups/byname/${encodeURIComponent(oldName)}`, {
|
||||
method: 'PUT',
|
||||
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:
|
||||
|
||||
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 { KanbanComponent as KanbanComponentType } from '@syncfusion/ej2-react-kanban';
|
||||
import { DialogComponent } from '@syncfusion/ej2-react-popups';
|
||||
import { useToast } from './components/ToastProvider';
|
||||
import { L10n } from '@syncfusion/ej2-base';
|
||||
|
||||
interface KanbanClient extends Client {
|
||||
Id: string;
|
||||
@@ -25,7 +27,46 @@ interface KanbanDragEventArgs {
|
||||
[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 toast = useToast();
|
||||
const [clients, setClients] = useState<KanbanClient[]>([]);
|
||||
const [groups, setGroups] = useState<{ keyField: string; headerText: string }[]>([]);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
@@ -74,11 +115,22 @@ const Infoscreen_groups: React.FC = () => {
|
||||
if (!newGroupName.trim()) return;
|
||||
try {
|
||||
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 }]);
|
||||
setNewGroupName('');
|
||||
setShowDialog(false);
|
||||
} catch {
|
||||
alert('Fehler beim Erstellen der Gruppe');
|
||||
} catch (err) {
|
||||
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);
|
||||
toast.show({
|
||||
content: de.groupDeleted,
|
||||
cssClass: 'e-toast-success',
|
||||
timeOut: 5000,
|
||||
showCloseButton: false,
|
||||
});
|
||||
// Gruppen und Clients neu laden
|
||||
const groupData = await fetchGroups();
|
||||
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}`,
|
||||
}))
|
||||
);
|
||||
} catch {
|
||||
alert('Fehler beim Löschen der Gruppe');
|
||||
} catch (err) {
|
||||
toast.show({
|
||||
content: (err as Error).message,
|
||||
cssClass: 'e-toast-danger',
|
||||
timeOut: 0,
|
||||
showCloseButton: true,
|
||||
});
|
||||
}
|
||||
setDeleteDialog({ open: false, groupName: '' });
|
||||
};
|
||||
@@ -117,6 +180,12 @@ const Infoscreen_groups: React.FC = () => {
|
||||
const handleRenameGroup = async () => {
|
||||
try {
|
||||
await renameGroup(renameDialog.oldName, renameDialog.newName);
|
||||
toast.show({
|
||||
content: de.groupRenamed,
|
||||
cssClass: 'e-toast-success',
|
||||
timeOut: 5000,
|
||||
showCloseButton: false,
|
||||
});
|
||||
// Gruppen und Clients neu laden
|
||||
const groupData = await fetchGroups();
|
||||
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}`,
|
||||
}))
|
||||
);
|
||||
} catch {
|
||||
alert('Fehler beim Umbenennen der Gruppe');
|
||||
} catch (err) {
|
||||
toast.show({
|
||||
content: (err as Error).message,
|
||||
cssClass: 'e-toast-danger',
|
||||
timeOut: 0,
|
||||
showCloseButton: true,
|
||||
});
|
||||
}
|
||||
setRenameDialog({ open: false, oldName: '', newName: '' });
|
||||
};
|
||||
@@ -217,28 +291,29 @@ const Infoscreen_groups: React.FC = () => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<button
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded"
|
||||
onClick={() => setShowDialog(true)}
|
||||
>
|
||||
Neue Raumgruppe
|
||||
{de.newGroup}
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-yellow-500 text-white rounded"
|
||||
onClick={() => setRenameDialog({ open: true, oldName: '', newName: '' })}
|
||||
>
|
||||
Gruppe umbenennen
|
||||
{de.renameGroup}
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-red-500 text-white rounded"
|
||||
onClick={() => setDeleteDialog({ open: true, groupName: '' })}
|
||||
>
|
||||
Gruppe löschen
|
||||
{de.deleteGroup}
|
||||
</button>
|
||||
</div>
|
||||
<KanbanComponent
|
||||
locale="de"
|
||||
id="kanban"
|
||||
keyField="Status"
|
||||
dataSource={clients}
|
||||
@@ -255,7 +330,7 @@ const Infoscreen_groups: React.FC = () => {
|
||||
{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">Neue Raumgruppe</h3>
|
||||
<h3 className="mb-2 font-bold">{de.newGroup}</h3>
|
||||
<input
|
||||
className="border p-2 mb-2 w-full"
|
||||
value={newGroupName}
|
||||
@@ -264,13 +339,13 @@ const Infoscreen_groups: React.FC = () => {
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button className="bg-blue-500 text-white px-4 py-2 rounded" onClick={handleAddGroup}>
|
||||
Hinzufügen
|
||||
{de.add}
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-300 px-4 py-2 rounded"
|
||||
onClick={() => setShowDialog(false)}
|
||||
>
|
||||
Abbrechen
|
||||
{de.cancel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -279,7 +354,7 @@ const Infoscreen_groups: React.FC = () => {
|
||||
{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">Raumgruppe umbenennen</h3>
|
||||
<h3 className="mb-2 font-bold">{de.renameGroup}</h3>
|
||||
<select
|
||||
className="border p-2 mb-2 w-full"
|
||||
value={renameDialog.oldName}
|
||||
@@ -291,7 +366,7 @@ const Infoscreen_groups: React.FC = () => {
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Gruppe wählen</option>
|
||||
<option value="">{de.selectGroup}</option>
|
||||
{groups
|
||||
.filter(g => g.headerText !== 'Nicht zugeordnet')
|
||||
.map(g => (
|
||||
@@ -304,7 +379,7 @@ const Infoscreen_groups: React.FC = () => {
|
||||
className="border p-2 mb-2 w-full"
|
||||
value={renameDialog.newName}
|
||||
onChange={e => setRenameDialog({ ...renameDialog, newName: e.target.value })}
|
||||
placeholder="Neuer Name"
|
||||
placeholder={de.newName}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
@@ -312,13 +387,13 @@ const Infoscreen_groups: React.FC = () => {
|
||||
onClick={handleRenameGroup}
|
||||
disabled={!renameDialog.oldName || !renameDialog.newName}
|
||||
>
|
||||
Umbenennen
|
||||
{de.rename}
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-300 px-4 py-2 rounded"
|
||||
onClick={() => setRenameDialog({ open: false, oldName: '', newName: '' })}
|
||||
>
|
||||
Abbrechen
|
||||
{de.cancel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -327,13 +402,13 @@ const Infoscreen_groups: React.FC = () => {
|
||||
{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">Gruppe löschen</h3>
|
||||
<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="">Gruppe wählen</option>
|
||||
<option value="">{de.selectGroup}</option>
|
||||
{groups
|
||||
.filter(g => g.headerText !== 'Nicht zugeordnet')
|
||||
.map(g => (
|
||||
@@ -342,10 +417,10 @@ const Infoscreen_groups: React.FC = () => {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p>Alle Clients werden in "Nicht zugeordnet" verschoben.</p>
|
||||
<p>{de.clientsMoved}</p>
|
||||
{deleteDialog.groupName && (
|
||||
<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?
|
||||
</div>
|
||||
)}
|
||||
@@ -355,20 +430,20 @@ const Infoscreen_groups: React.FC = () => {
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={!deleteDialog.groupName}
|
||||
>
|
||||
Löschen
|
||||
{de.deleteGroup}
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-300 px-4 py-2 rounded"
|
||||
onClick={() => setDeleteDialog({ open: false, groupName: '' })}
|
||||
>
|
||||
Abbrechen
|
||||
{de.cancel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showDeleteConfirm && deleteDialog.groupName && (
|
||||
<DialogComponent
|
||||
width="350px"
|
||||
header="Löschbestätigung"
|
||||
header={de.confirmDelete}
|
||||
visible={showDeleteConfirm}
|
||||
close={() => setShowDeleteConfirm(false)}
|
||||
footerTemplate={() => (
|
||||
@@ -380,7 +455,7 @@ const Infoscreen_groups: React.FC = () => {
|
||||
setShowDeleteConfirm(false);
|
||||
}}
|
||||
>
|
||||
Ja, löschen
|
||||
{de.yesDelete}
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-300 px-4 py-2 rounded"
|
||||
@@ -389,7 +464,7 @@ const Infoscreen_groups: React.FC = () => {
|
||||
setDeleteDialog({ open: false, groupName: '' });
|
||||
}}
|
||||
>
|
||||
Abbrechen
|
||||
{de.cancel}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -397,9 +472,7 @@ const Infoscreen_groups: React.FC = () => {
|
||||
<div>
|
||||
Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b> wirklich löschen?
|
||||
<br />
|
||||
<span className="text-sm text-gray-500">
|
||||
Alle Clients werden in "Nicht zugeordnet" verschoben.
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">{de.clientsMoved}</span>
|
||||
</div>
|
||||
</DialogComponent>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from database import Session
|
||||
from models import ClientGroup
|
||||
from flask import Blueprint, request, jsonify
|
||||
from sqlalchemy import func
|
||||
import sys
|
||||
sys.path.append('/workspace')
|
||||
|
||||
@@ -112,9 +113,9 @@ def rename_group_by_name(old_name):
|
||||
return jsonify({"error": "Gruppe nicht gefunden"}), 404
|
||||
|
||||
# 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()
|
||||
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
|
||||
session.commit()
|
||||
|
||||
Reference in New Issue
Block a user