Files
dashboard/frontend/src/components/admin/BannerManagementTab.tsx
2026-03-12 16:05:01 +01:00

258 lines
7.9 KiB
TypeScript

import { useState } from 'react';
import {
Box,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
IconButton,
Typography,
CircularProgress,
Select,
MenuItem,
FormControl,
InputLabel,
Chip,
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { bannerApi } from '../../services/banners';
import { useNotification } from '../../contexts/NotificationContext';
import type { BannerLevel, BannerShowAs } from '../../types/banner.types';
const LEVEL_LABEL: Record<BannerLevel, string> = {
info: 'Info',
important: 'Wichtig',
critical: 'Kritisch',
};
const LEVEL_COLOR: Record<BannerLevel, 'info' | 'warning' | 'error'> = {
info: 'info',
important: 'warning',
critical: 'error',
};
function formatDateTime(iso: string | null | undefined): string {
if (!iso) return 'Kein Ablauf';
return new Date(iso).toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
const SHOW_AS_LABEL: Record<BannerShowAs, string> = {
banner: 'Banner',
widget: 'Widget',
};
function BannerManagementTab() {
const queryClient = useQueryClient();
const { showSuccess, showError } = useNotification();
const [dialogOpen, setDialogOpen] = useState(false);
const [newMessage, setNewMessage] = useState('');
const [newLevel, setNewLevel] = useState<BannerLevel>('info');
const [newEndsAt, setNewEndsAt] = useState('');
const [newShowAs, setNewShowAs] = useState<BannerShowAs>('banner');
const { data: banners, isLoading } = useQuery({
queryKey: ['admin', 'banners'],
queryFn: bannerApi.getAll,
placeholderData: (previousData: any) => previousData,
});
const createMutation = useMutation({
mutationFn: () =>
bannerApi.create({
message: newMessage.trim(),
level: newLevel,
show_as: newShowAs,
starts_at: new Date().toISOString(),
ends_at: newEndsAt ? new Date(newEndsAt).toISOString() : null,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'banners'] });
queryClient.invalidateQueries({ queryKey: ['banners', 'active'] });
showSuccess('Banner wurde erstellt');
setDialogOpen(false);
setNewMessage('');
setNewLevel('info');
setNewEndsAt('');
setNewShowAs('banner');
},
onError: (error: any) => {
const message = error?.response?.data?.message || 'Banner konnte nicht erstellt werden';
showError(message);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => bannerApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'banners'] });
queryClient.invalidateQueries({ queryKey: ['banners', 'active'] });
showSuccess('Banner wurde gelöscht');
},
onError: () => {
showError('Banner konnte nicht gelöscht werden');
},
});
const handleCreate = () => {
if (newMessage.trim()) {
createMutation.mutate();
}
};
const handleClose = () => {
setDialogOpen(false);
setNewMessage('');
setNewLevel('info');
setNewEndsAt('');
setNewShowAs('banner');
};
if (isLoading) {
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
}
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">Ankündigungsbanner</Typography>
<Button startIcon={<AddIcon />} variant="contained" onClick={() => setDialogOpen(true)}>
Banner erstellen
</Button>
</Box>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Stufe</TableCell>
<TableCell>Anzeige</TableCell>
<TableCell>Nachricht</TableCell>
<TableCell>Erstellt am</TableCell>
<TableCell>Ablauf</TableCell>
<TableCell>Aktionen</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(banners ?? []).map((banner) => (
<TableRow key={banner.id}>
<TableCell>
<Chip
label={LEVEL_LABEL[banner.level]}
color={LEVEL_COLOR[banner.level]}
size="small"
/>
</TableCell>
<TableCell>
<Chip
label={SHOW_AS_LABEL[banner.show_as] ?? 'Banner'}
variant="outlined"
size="small"
/>
</TableCell>
<TableCell sx={{ maxWidth: 400 }}>{banner.message}</TableCell>
<TableCell>{formatDateTime(banner.created_at)}</TableCell>
<TableCell>{formatDateTime(banner.ends_at)}</TableCell>
<TableCell>
<IconButton
size="small"
color="error"
onClick={() => deleteMutation.mutate(banner.id)}
disabled={deleteMutation.isPending}
>
<DeleteIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
))}
{(banners ?? []).length === 0 && (
<TableRow>
<TableCell colSpan={6} align="center">Keine Banner vorhanden</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<Dialog open={dialogOpen} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>Banner erstellen</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="Nachricht"
fullWidth
multiline
minRows={3}
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
inputProps={{ maxLength: 2000 }}
helperText={`${newMessage.length}/2000`}
/>
<FormControl fullWidth margin="dense">
<InputLabel>Stufe</InputLabel>
<Select
value={newLevel}
label="Stufe"
onChange={(e) => setNewLevel(e.target.value as BannerLevel)}
>
<MenuItem value="info">Info</MenuItem>
<MenuItem value="important">Wichtig</MenuItem>
<MenuItem value="critical">Kritisch</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth margin="dense">
<InputLabel>Anzeige als</InputLabel>
<Select
value={newShowAs}
label="Anzeige als"
onChange={(e) => setNewShowAs(e.target.value as BannerShowAs)}
>
<MenuItem value="banner">Banner</MenuItem>
<MenuItem value="widget">Widget</MenuItem>
</Select>
</FormControl>
<TextField
margin="dense"
label="Ablaufdatum (optional)"
type="datetime-local"
fullWidth
value={newEndsAt}
onChange={(e) => setNewEndsAt(e.target.value)}
InputLabelProps={{ shrink: true }}
helperText="Leer lassen für kein Ablaufdatum"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Abbrechen</Button>
<Button
onClick={handleCreate}
variant="contained"
disabled={createMutation.isPending || !newMessage.trim()}
>
Erstellen
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
export default BannerManagementTab;