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 <noreply@anthropic.com>
This commit is contained in:
@@ -183,6 +183,7 @@ function Atemschutz() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
if (canViewAll) {
|
||||||
const [traegerData, statsData, membersData] = await Promise.all([
|
const [traegerData, statsData, membersData] = await Promise.all([
|
||||||
atemschutzApi.getAll(),
|
atemschutzApi.getAll(),
|
||||||
atemschutzApi.getStats(),
|
atemschutzApi.getStats(),
|
||||||
@@ -191,12 +192,16 @@ function Atemschutz() {
|
|||||||
setTraeger(traegerData);
|
setTraeger(traegerData);
|
||||||
setStats(statsData);
|
setStats(statsData);
|
||||||
setMembers(membersData.items);
|
setMembers(membersData.items);
|
||||||
|
} else {
|
||||||
|
const traegerData = await atemschutzApi.getAll();
|
||||||
|
setTraeger(traegerData);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setError('Atemschutzdaten konnten nicht geladen werden. Bitte versuchen Sie es erneut.');
|
setError('Atemschutzdaten konnten nicht geladen werden. Bitte versuchen Sie es erneut.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [canViewAll]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -361,6 +366,11 @@ function Atemschutz() {
|
|||||||
<Typography variant="h4" gutterBottom sx={{ mb: 0 }}>
|
<Typography variant="h4" gutterBottom sx={{ mb: 0 }}>
|
||||||
Atemschutzverwaltung
|
Atemschutzverwaltung
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{!loading && !canViewAll && (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||||
|
Dein persönlicher Atemschutz-Status
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
{!loading && stats && canViewAll && (
|
{!loading && stats && canViewAll && (
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 0.5 }}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 0.5 }}>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
@@ -471,7 +481,7 @@ function Atemschutz() {
|
|||||||
<TableCell>Untersuchung gültig bis</TableCell>
|
<TableCell>Untersuchung gültig bis</TableCell>
|
||||||
<TableCell>Leistungstest gültig bis</TableCell>
|
<TableCell>Leistungstest gültig bis</TableCell>
|
||||||
<TableCell align="center">Status</TableCell>
|
<TableCell align="center">Status</TableCell>
|
||||||
<TableCell align="right">Aktionen</TableCell>
|
{canWrite && <TableCell align="right">Aktionen</TableCell>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -549,8 +559,8 @@ function Atemschutz() {
|
|||||||
variant="filled"
|
variant="filled"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">
|
|
||||||
{canWrite && (
|
{canWrite && (
|
||||||
|
<TableCell align="right">
|
||||||
<Tooltip title="Bearbeiten">
|
<Tooltip title="Bearbeiten">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
@@ -560,8 +570,6 @@ function Atemschutz() {
|
|||||||
<Edit fontSize="small" />
|
<Edit fontSize="small" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
|
||||||
{canWrite && (
|
|
||||||
<Tooltip title="Löschen">
|
<Tooltip title="Löschen">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
@@ -572,8 +580,8 @@ function Atemschutz() {
|
|||||||
<Delete fontSize="small" />
|
<Delete fontSize="small" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function Dashboard() {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
|
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
||||||
gap: 2.5,
|
gap: 2.5,
|
||||||
alignItems: 'start',
|
alignItems: 'start',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ContentCopy as CopyIcon,
|
ContentCopy as CopyIcon,
|
||||||
|
DeleteForever as DeleteForeverIcon,
|
||||||
DirectionsCar as CarIcon,
|
DirectionsCar as CarIcon,
|
||||||
Edit as EditIcon,
|
Edit as EditIcon,
|
||||||
Event as EventIcon,
|
Event as EventIcon,
|
||||||
@@ -453,11 +454,12 @@ interface DayPopoverProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onTrainingClick: (id: string) => void;
|
onTrainingClick: (id: string) => void;
|
||||||
onEventEdit: (ev: VeranstaltungListItem) => void;
|
onEventEdit: (ev: VeranstaltungListItem) => void;
|
||||||
|
onEventDelete: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DayPopover({
|
function DayPopover({
|
||||||
anchorEl, day, trainingForDay, eventsForDay,
|
anchorEl, day, trainingForDay, eventsForDay,
|
||||||
canWriteEvents, onClose, onTrainingClick, onEventEdit,
|
canWriteEvents, onClose, onTrainingClick, onEventEdit, onEventDelete,
|
||||||
}: DayPopoverProps) {
|
}: DayPopoverProps) {
|
||||||
if (!day) return null;
|
if (!day) return null;
|
||||||
const hasContent = trainingForDay.length > 0 || eventsForDay.length > 0;
|
const hasContent = trainingForDay.length > 0 || eventsForDay.length > 0;
|
||||||
@@ -589,6 +591,15 @@ function DayPopover({
|
|||||||
<EditIcon fontSize="small" />
|
<EditIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
{canWriteEvents && (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
onClick={() => { onEventDelete(ev.id); onClose(); }}
|
||||||
|
>
|
||||||
|
<DeleteForeverIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
@@ -610,11 +621,12 @@ interface CombinedListViewProps {
|
|||||||
onTrainingClick: (id: string) => void;
|
onTrainingClick: (id: string) => void;
|
||||||
onEventEdit: (ev: VeranstaltungListItem) => void;
|
onEventEdit: (ev: VeranstaltungListItem) => void;
|
||||||
onEventCancel: (id: string) => void;
|
onEventCancel: (id: string) => void;
|
||||||
|
onEventDelete: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CombinedListView({
|
function CombinedListView({
|
||||||
trainingEvents, veranstaltungen, selectedKategorie,
|
trainingEvents, veranstaltungen, selectedKategorie,
|
||||||
canWriteEvents, onTrainingClick, onEventEdit, onEventCancel,
|
canWriteEvents, onTrainingClick, onEventEdit, onEventCancel, onEventDelete,
|
||||||
}: CombinedListViewProps) {
|
}: CombinedListViewProps) {
|
||||||
type ListEntry =
|
type ListEntry =
|
||||||
| { kind: 'training'; item: UebungListItem }
|
| { kind: 'training'; item: UebungListItem }
|
||||||
@@ -742,6 +754,18 @@ function CombinedListView({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
{!isTraining && canWriteEvents && (
|
||||||
|
<Box sx={{ display: 'flex', gap: 0.5, ml: item.abgesagt ? 1 : 0 }}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
onClick={() => onEventDelete(item.id)}
|
||||||
|
title="Endgültig löschen"
|
||||||
|
>
|
||||||
|
<DeleteForeverIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -1141,6 +1165,8 @@ export default function Kalender() {
|
|||||||
const [cancelEventId, setCancelEventId] = useState<string | null>(null);
|
const [cancelEventId, setCancelEventId] = useState<string | null>(null);
|
||||||
const [cancelEventGrund, setCancelEventGrund] = useState('');
|
const [cancelEventGrund, setCancelEventGrund] = useState('');
|
||||||
const [cancelEventLoading, setCancelEventLoading] = useState(false);
|
const [cancelEventLoading, setCancelEventLoading] = useState(false);
|
||||||
|
const [deleteEventId, setDeleteEventId] = useState<string | null>(null);
|
||||||
|
const [deleteEventLoading, setDeleteEventLoading] = useState(false);
|
||||||
|
|
||||||
// ── Bookings tab state ───────────────────────────────────────────────────────
|
// ── Bookings tab state ───────────────────────────────────────────────────────
|
||||||
const [currentWeekStart, setCurrentWeekStart] = useState<Date>(() =>
|
const [currentWeekStart, setCurrentWeekStart] = useState<Date>(() =>
|
||||||
@@ -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 ──────────────────────────────────────────────────────────
|
// ── Booking helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const getBookingsForCell = (vehicleId: string, day: Date): FahrzeugBuchungListItem[] =>
|
const getBookingsForCell = (vehicleId: string, day: Date): FahrzeugBuchungListItem[] =>
|
||||||
@@ -1642,6 +1683,7 @@ export default function Kalender() {
|
|||||||
setCancelEventId(id);
|
setCancelEventId(id);
|
||||||
setCancelEventGrund('');
|
setCancelEventGrund('');
|
||||||
}}
|
}}
|
||||||
|
onEventDelete={(id) => setDeleteEventId(id)}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
)}
|
)}
|
||||||
@@ -1673,6 +1715,7 @@ export default function Kalender() {
|
|||||||
setVeranstEditing(ev);
|
setVeranstEditing(ev);
|
||||||
setVeranstFormOpen(true);
|
setVeranstFormOpen(true);
|
||||||
}}
|
}}
|
||||||
|
onEventDelete={(id) => setDeleteEventId(id)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Veranstaltung Form Dialog */}
|
{/* Veranstaltung Form Dialog */}
|
||||||
@@ -1720,6 +1763,34 @@ export default function Kalender() {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Endgültig löschen Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={Boolean(deleteEventId)}
|
||||||
|
onClose={() => setDeleteEventId(null)}
|
||||||
|
maxWidth="xs"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>Veranstaltung endgültig löschen?</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
Diese Veranstaltung wird endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDeleteEventId(null)} disabled={deleteEventLoading}>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={handleDeleteEvent}
|
||||||
|
disabled={deleteEventLoading}
|
||||||
|
>
|
||||||
|
{deleteEventLoading ? <CircularProgress size={20} /> : 'Endgültig löschen'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* iCal Event subscription dialog */}
|
{/* iCal Event subscription dialog */}
|
||||||
<Dialog open={icalEventOpen} onClose={() => setIcalEventOpen(false)} maxWidth="sm" fullWidth>
|
<Dialog open={icalEventOpen} onClose={() => setIcalEventOpen(false)} maxWidth="sm" fullWidth>
|
||||||
<DialogTitle>Kalender abonnieren</DialogTitle>
|
<DialogTitle>Kalender abonnieren</DialogTitle>
|
||||||
|
|||||||
Reference in New Issue
Block a user