258 lines
7.9 KiB
TypeScript
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;
|