bug fixes
This commit is contained in:
@@ -152,7 +152,9 @@ const StatCard: React.FC<StatCardProps> = ({ label, value, color, bgcolor }) =>
|
||||
function Atemschutz() {
|
||||
const notification = useNotification();
|
||||
const { user } = useAuth();
|
||||
const canWrite = user?.groups?.some(g => ['dashboard_admin', 'dashboard_atemschutz'].includes(g)) ?? false;
|
||||
const ATEMSCHUTZ_PRIVILEGED = ['dashboard_admin', 'dashboard_kommando', 'dashboard_atemschutz', 'dashboard_moderator'];
|
||||
const canViewAll = user?.groups?.some(g => ATEMSCHUTZ_PRIVILEGED.includes(g)) ?? false;
|
||||
const canWrite = canViewAll;
|
||||
|
||||
// Data state
|
||||
const [traeger, setTraeger] = useState<AtemschutzUebersicht[]>([]);
|
||||
@@ -359,7 +361,7 @@ function Atemschutz() {
|
||||
<Typography variant="h4" gutterBottom sx={{ mb: 0 }}>
|
||||
Atemschutzverwaltung
|
||||
</Typography>
|
||||
{!loading && stats && (
|
||||
{!loading && stats && canViewAll && (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 0.5 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{stats.total} Gesamt
|
||||
@@ -382,7 +384,7 @@ function Atemschutz() {
|
||||
</Box>
|
||||
|
||||
{/* Stats cards */}
|
||||
{!loading && stats && (
|
||||
{!loading && stats && canViewAll && (
|
||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<StatCard
|
||||
@@ -405,6 +407,7 @@ function Atemschutz() {
|
||||
)}
|
||||
|
||||
{/* Search bar */}
|
||||
{canViewAll && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<TextField
|
||||
placeholder="Suchen (Name, E-Mail, Dienstgrad...)"
|
||||
@@ -421,6 +424,7 @@ function Atemschutz() {
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Loading state */}
|
||||
{loading && (
|
||||
|
||||
@@ -33,12 +33,7 @@ function Dashboard() {
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: {
|
||||
xs: '1fr',
|
||||
sm: 'repeat(2, 1fr)',
|
||||
lg: 'repeat(3, 1fr)',
|
||||
xl: 'repeat(4, 1fr)',
|
||||
},
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
|
||||
gap: 2.5,
|
||||
alignItems: 'start',
|
||||
}}
|
||||
|
||||
@@ -41,6 +41,8 @@ function Profile() {
|
||||
});
|
||||
};
|
||||
|
||||
const dashboardGroups = (user.groups ?? []).filter((g) => g.startsWith('dashboard_'));
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Container maxWidth="lg">
|
||||
@@ -93,7 +95,7 @@ function Profile() {
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
{/* Groups/Roles */}
|
||||
{user.groups && user.groups.length > 0 && (
|
||||
{dashboardGroups.length > 0 && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
@@ -105,9 +107,11 @@ function Profile() {
|
||||
Gruppen
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 1 }}>
|
||||
{user.groups.map((group) => (
|
||||
<Chip key={group} label={group} size="small" color="primary" />
|
||||
))}
|
||||
{dashboardGroups.map((group) => {
|
||||
const name = group.replace(/^dashboard_/, '');
|
||||
const label = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
return <Chip key={group} label={label} size="small" color="primary" />;
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
Today as TodayIcon,
|
||||
IosShare,
|
||||
Event as EventIcon,
|
||||
Delete as DeleteIcon,
|
||||
} from '@mui/icons-material';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
@@ -851,9 +852,10 @@ interface ListViewProps {
|
||||
canWrite: boolean;
|
||||
onEdit: (ev: VeranstaltungListItem) => void;
|
||||
onCancel: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
function EventListView({ events, canWrite, onEdit, onCancel }: ListViewProps) {
|
||||
function EventListView({ events, canWrite, onEdit, onCancel, onDelete }: ListViewProps) {
|
||||
if (events.length === 0) {
|
||||
return (
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
@@ -945,9 +947,16 @@ function EventListView({ events, canWrite, onEdit, onCancel }: ListViewProps) {
|
||||
<IconButton size="small" onClick={() => onEdit(ev)}>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton size="small" color="error" onClick={() => onCancel(ev.id)}>
|
||||
<CancelIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Tooltip title="Stornieren">
|
||||
<IconButton size="small" color="error" onClick={() => onCancel(ev.id)}>
|
||||
<CancelIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Löschen">
|
||||
<IconButton size="small" color="error" onClick={() => onDelete(ev.id)}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
</ListItem>
|
||||
@@ -996,6 +1005,10 @@ export default function Veranstaltungen() {
|
||||
const [cancelGrund, setCancelGrund] = useState('');
|
||||
const [cancelLoading, setCancelLoading] = useState(false);
|
||||
|
||||
// Delete dialog
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
|
||||
// iCal dialog
|
||||
const [icalOpen, setIcalOpen] = useState(false);
|
||||
|
||||
@@ -1100,6 +1113,22 @@ export default function Veranstaltungen() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteEvent = async () => {
|
||||
if (!deleteId) return;
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
await eventsApi.deleteEvent(deleteId);
|
||||
setDeleteId(null);
|
||||
loadData();
|
||||
notification.showSuccess('Veranstaltung wurde gelöscht');
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Löschen';
|
||||
notification.showError(msg);
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filtered events for list view
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1246,6 +1275,7 @@ export default function Veranstaltungen() {
|
||||
canWrite={canWrite}
|
||||
onEdit={(ev) => { setEditingEvent(ev); setFormOpen(true); }}
|
||||
onCancel={(id) => { setCancelId(id); setCancelGrund(''); }}
|
||||
onDelete={(id) => setDeleteId(id)}
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
@@ -1289,16 +1319,16 @@ export default function Veranstaltungen() {
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Veranstaltung absagen</DialogTitle>
|
||||
<DialogTitle>Veranstaltung stornieren</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText sx={{ mb: 2 }}>
|
||||
Bitte gib einen Grund für die Absage an (mind. 5 Zeichen).
|
||||
Bitte gib einen Grund für die Stornierung an (mind. 5 Zeichen).
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
label="Absagegrund"
|
||||
label="Stornierungsgrund"
|
||||
value={cancelGrund}
|
||||
onChange={(e) => setCancelGrund(e.target.value)}
|
||||
autoFocus
|
||||
@@ -1312,7 +1342,23 @@ export default function Veranstaltungen() {
|
||||
onClick={handleCancelEvent}
|
||||
disabled={cancelGrund.trim().length < 5 || cancelLoading}
|
||||
>
|
||||
{cancelLoading ? <CircularProgress size={20} /> : 'Absagen'}
|
||||
{cancelLoading ? <CircularProgress size={20} /> : 'Stornieren'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Dialog */}
|
||||
<Dialog open={Boolean(deleteId)} onClose={() => setDeleteId(null)} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>Veranstaltung endgültig löschen</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Soll diese Veranstaltung wirklich endgültig gelöscht werden? Diese Aktion kann nicht rückgängig gemacht werden.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteId(null)}>Abbrechen</Button>
|
||||
<Button variant="contained" color="error" onClick={handleDeleteEvent} disabled={deleteLoading}>
|
||||
{deleteLoading ? <CircularProgress size={20} /> : 'Endgültig löschen'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -129,6 +129,11 @@ export const eventsApi = {
|
||||
.then(() => undefined);
|
||||
},
|
||||
|
||||
/** Hard-delete an event permanently */
|
||||
deleteEvent(id: string): Promise<void> {
|
||||
return api.post(`/api/events/${id}/delete`).then(() => undefined);
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// iCal
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user