From b3a2fd9ff96390c9d95e4111e0fad9302690cf96 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Tue, 3 Mar 2026 14:59:08 +0100 Subject: [PATCH] feat: responsive widgets, atemschutz permission UX, event hard-delete - fix dashboard grid: use auto-fill instead of auto-fit for equal-width widgets - atemschutz: skip stats/members API calls for non-privileged users, hide empty Aktionen column, add personal status subtitle - kalender: add permanent delete option for events with confirmation dialog Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/Atemschutz.tsx | 78 +++++++++++++++++-------------- frontend/src/pages/Dashboard.tsx | 2 +- frontend/src/pages/Kalender.tsx | 75 ++++++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 38 deletions(-) diff --git a/frontend/src/pages/Atemschutz.tsx b/frontend/src/pages/Atemschutz.tsx index fbbc034..a4643ad 100644 --- a/frontend/src/pages/Atemschutz.tsx +++ b/frontend/src/pages/Atemschutz.tsx @@ -183,20 +183,25 @@ function Atemschutz() { try { setLoading(true); setError(null); - const [traegerData, statsData, membersData] = await Promise.all([ - atemschutzApi.getAll(), - atemschutzApi.getStats(), - membersService.getMembers({ pageSize: 500 }), - ]); - setTraeger(traegerData); - setStats(statsData); - setMembers(membersData.items); + if (canViewAll) { + const [traegerData, statsData, membersData] = await Promise.all([ + atemschutzApi.getAll(), + atemschutzApi.getStats(), + membersService.getMembers({ pageSize: 500 }), + ]); + setTraeger(traegerData); + setStats(statsData); + setMembers(membersData.items); + } else { + const traegerData = await atemschutzApi.getAll(); + setTraeger(traegerData); + } } catch { setError('Atemschutzdaten konnten nicht geladen werden. Bitte versuchen Sie es erneut.'); } finally { setLoading(false); } - }, []); + }, [canViewAll]); useEffect(() => { fetchData(); @@ -361,6 +366,11 @@ function Atemschutz() { Atemschutzverwaltung + {!loading && !canViewAll && ( + + Dein persönlicher Atemschutz-Status + + )} {!loading && stats && canViewAll && ( @@ -471,7 +481,7 @@ function Atemschutz() { Untersuchung gültig bis Leistungstest gültig bis Status - Aktionen + {canWrite && Aktionen} @@ -549,31 +559,29 @@ function Atemschutz() { variant="filled" /> - - {canWrite && ( - - - - )} - {canWrite && ( - - - - )} - + {canWrite && ( + + + + + + + + + )} ); })} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index fe20683..bce7f99 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -33,7 +33,7 @@ function Dashboard() { void; onTrainingClick: (id: string) => void; onEventEdit: (ev: VeranstaltungListItem) => void; + onEventDelete: (id: string) => void; } function DayPopover({ anchorEl, day, trainingForDay, eventsForDay, - canWriteEvents, onClose, onTrainingClick, onEventEdit, + canWriteEvents, onClose, onTrainingClick, onEventEdit, onEventDelete, }: DayPopoverProps) { if (!day) return null; const hasContent = trainingForDay.length > 0 || eventsForDay.length > 0; @@ -589,6 +591,15 @@ function DayPopover({ )} + {canWriteEvents && ( + { onEventDelete(ev.id); onClose(); }} + > + + + )} ))} @@ -610,11 +621,12 @@ interface CombinedListViewProps { onTrainingClick: (id: string) => void; onEventEdit: (ev: VeranstaltungListItem) => void; onEventCancel: (id: string) => void; + onEventDelete: (id: string) => void; } function CombinedListView({ trainingEvents, veranstaltungen, selectedKategorie, - canWriteEvents, onTrainingClick, onEventEdit, onEventCancel, + canWriteEvents, onTrainingClick, onEventEdit, onEventCancel, onEventDelete, }: CombinedListViewProps) { type ListEntry = | { kind: 'training'; item: UebungListItem } @@ -742,6 +754,18 @@ function CombinedListView({ )} + {!isTraining && canWriteEvents && ( + + onEventDelete(item.id)} + title="Endgültig löschen" + > + + + + )} ); @@ -1141,6 +1165,8 @@ export default function Kalender() { const [cancelEventId, setCancelEventId] = useState(null); const [cancelEventGrund, setCancelEventGrund] = useState(''); const [cancelEventLoading, setCancelEventLoading] = useState(false); + const [deleteEventId, setDeleteEventId] = useState(null); + const [deleteEventLoading, setDeleteEventLoading] = useState(false); // ── Bookings tab state ─────────────────────────────────────────────────────── const [currentWeekStart, setCurrentWeekStart] = useState(() => @@ -1326,6 +1352,21 @@ export default function Kalender() { } }; + const handleDeleteEvent = async () => { + if (!deleteEventId) return; + setDeleteEventLoading(true); + try { + await eventsApi.deleteEvent(deleteEventId); + notification.showSuccess('Veranstaltung wurde endgültig gelöscht'); + setDeleteEventId(null); + loadCalendarData(); + } catch (e: unknown) { + notification.showError((e as any)?.message || 'Fehler beim Löschen'); + } finally { + setDeleteEventLoading(false); + } + }; + // ── Booking helpers ────────────────────────────────────────────────────────── const getBookingsForCell = (vehicleId: string, day: Date): FahrzeugBuchungListItem[] => @@ -1642,6 +1683,7 @@ export default function Kalender() { setCancelEventId(id); setCancelEventGrund(''); }} + onEventDelete={(id) => setDeleteEventId(id)} /> )} @@ -1673,6 +1715,7 @@ export default function Kalender() { setVeranstEditing(ev); setVeranstFormOpen(true); }} + onEventDelete={(id) => setDeleteEventId(id)} /> {/* Veranstaltung Form Dialog */} @@ -1720,6 +1763,34 @@ export default function Kalender() { + {/* Endgültig löschen Dialog */} + setDeleteEventId(null)} + maxWidth="xs" + fullWidth + > + Veranstaltung endgültig löschen? + + + Diese Veranstaltung wird endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden. + + + + + + + + {/* iCal Event subscription dialog */} setIcalEventOpen(false)} maxWidth="sm" fullWidth> Kalender abonnieren