This commit is contained in:
Matthias Hochmeister
2026-03-16 15:01:09 +01:00
parent 3c72fe627f
commit f3ad989a9e
28 changed files with 794 additions and 52 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import {
Box,
TextField,
@@ -11,20 +11,49 @@ import {
DialogContentText,
DialogActions,
CircularProgress,
FormControlLabel,
Checkbox,
Chip,
OutlinedInput,
InputLabel,
FormControl,
Select,
} from '@mui/material';
import type { SelectChangeEvent } from '@mui/material';
import SendIcon from '@mui/icons-material/Send';
import PeopleIcon from '@mui/icons-material/People';
import { useMutation } from '@tanstack/react-query';
import { adminApi } from '../../services/admin';
import { useNotification } from '../../contexts/NotificationContext';
import type { BroadcastPayload } from '../../types/admin.types';
const DIENSTGRAD_OPTIONS = [
'Mitglied',
'Maschinist',
'Truppführer',
'Gruppenführer',
'Zugkommandant',
'Kommandant',
];
const GROUP_OPTIONS = [
'dashboard_admin',
'dashboard_kommando',
'dashboard_gruppenfuehrer',
];
function NotificationBroadcastTab() {
const { showSuccess, showError } = useNotification();
const [titel, setTitel] = useState('');
const [nachricht, setNachricht] = useState('');
const [schwere, setSchwere] = useState<'info' | 'warnung' | 'fehler'>('info');
const [targetGroup, setTargetGroup] = useState('');
const [targetDienstgrad, setTargetDienstgrad] = useState<string[]>([]);
const [alleBenutzer, setAlleBenutzer] = useState(true);
const [confirmOpen, setConfirmOpen] = useState(false);
const [previewCount, setPreviewCount] = useState<number | null>(null);
const [previewLoading, setPreviewLoading] = useState(false);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const broadcastMutation = useMutation({
mutationFn: (data: BroadcastPayload) => adminApi.broadcast(data),
@@ -34,26 +63,74 @@ function NotificationBroadcastTab() {
setNachricht('');
setSchwere('info');
setTargetGroup('');
setTargetDienstgrad([]);
setAlleBenutzer(true);
setPreviewCount(null);
},
onError: () => {
showError('Fehler beim Senden der Benachrichtigung');
},
});
const fetchPreview = useCallback(() => {
if (alleBenutzer) {
// For "Alle Benutzer" we still fetch the count (no filters)
setPreviewLoading(true);
adminApi.broadcastPreview({})
.then((result) => setPreviewCount(result.count))
.catch(() => setPreviewCount(null))
.finally(() => setPreviewLoading(false));
return;
}
const payload: { targetGroup?: string; targetDienstgrad?: string[] } = {};
if (targetGroup.trim()) payload.targetGroup = targetGroup.trim();
if (targetDienstgrad.length > 0) payload.targetDienstgrad = targetDienstgrad;
setPreviewLoading(true);
adminApi.broadcastPreview(payload)
.then((result) => setPreviewCount(result.count))
.catch(() => setPreviewCount(null))
.finally(() => setPreviewLoading(false));
}, [alleBenutzer, targetGroup, targetDienstgrad]);
useEffect(() => {
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(fetchPreview, 300);
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [fetchPreview]);
const handleSubmit = () => {
setConfirmOpen(true);
};
const handleConfirm = () => {
setConfirmOpen(false);
broadcastMutation.mutate({
titel,
nachricht,
schwere,
...(targetGroup.trim() ? { targetGroup: targetGroup.trim() } : {}),
});
const payload: BroadcastPayload = { titel, nachricht, schwere };
if (!alleBenutzer) {
if (targetGroup.trim()) payload.targetGroup = targetGroup.trim();
if (targetDienstgrad.length > 0) payload.targetDienstgrad = targetDienstgrad;
}
broadcastMutation.mutate(payload);
};
const handleDienstgradChange = (event: SelectChangeEvent<string[]>) => {
const value = event.target.value;
setTargetDienstgrad(typeof value === 'string' ? value.split(',') : value);
};
const filtersActive = !alleBenutzer && (targetGroup.trim() || targetDienstgrad.length > 0);
const filterDescription = (() => {
if (alleBenutzer) return 'alle aktiven Benutzer';
const parts: string[] = [];
if (targetGroup.trim()) parts.push(`Gruppe "${targetGroup.trim()}"`);
if (targetDienstgrad.length > 0) parts.push(`Dienstgrad: ${targetDienstgrad.join(', ')}`);
return parts.length > 0 ? parts.join(' + ') : 'alle aktiven Benutzer';
})();
return (
<Box sx={{ maxWidth: 600 }}>
<Typography variant="h6" sx={{ mb: 2 }}>Benachrichtigung senden</Typography>
@@ -91,15 +168,77 @@ function NotificationBroadcastTab() {
<MenuItem value="fehler">Fehler</MenuItem>
</TextField>
<TextField
label="Zielgruppe (optional)"
fullWidth
value={targetGroup}
onChange={(e) => setTargetGroup(e.target.value)}
helperText="Leer lassen um an alle aktiven Benutzer zu senden"
sx={{ mb: 3 }}
<FormControlLabel
control={
<Checkbox
checked={alleBenutzer}
onChange={(e) => {
setAlleBenutzer(e.target.checked);
if (e.target.checked) {
setTargetGroup('');
setTargetDienstgrad([]);
}
}}
/>
}
label="Alle Benutzer"
sx={{ mb: 2, display: 'block' }}
/>
{!alleBenutzer && (
<>
<TextField
select
label="Authentik-Gruppe"
fullWidth
value={targetGroup}
onChange={(e) => setTargetGroup(e.target.value)}
sx={{ mb: 2 }}
>
<MenuItem value="">
<em>Keine Einschraenkung</em>
</MenuItem>
{GROUP_OPTIONS.map((g) => (
<MenuItem key={g} value={g}>{g}</MenuItem>
))}
</TextField>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Dienstgrad</InputLabel>
<Select
multiple
value={targetDienstgrad}
onChange={handleDienstgradChange}
input={<OutlinedInput label="Dienstgrad" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} size="small" />
))}
</Box>
)}
>
{DIENSTGRAD_OPTIONS.map((d) => (
<MenuItem key={d} value={d}>{d}</MenuItem>
))}
</Select>
</FormControl>
</>
)}
<Box sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<PeopleIcon color="action" fontSize="small" />
{previewLoading ? (
<CircularProgress size={16} />
) : (
<Typography variant="body2" color="text.secondary">
{previewCount !== null
? `Wird an ${previewCount} Benutzer gesendet`
: 'Empfaengeranzahl wird geladen...'}
</Typography>
)}
</Box>
<Button
variant="contained"
startIcon={broadcastMutation.isPending ? <CircularProgress size={18} color="inherit" /> : <SendIcon />}
@@ -113,8 +252,10 @@ function NotificationBroadcastTab() {
<DialogTitle>Benachrichtigung senden?</DialogTitle>
<DialogContent>
<DialogContentText>
Sind Sie sicher, dass Sie diese Benachrichtigung
{targetGroup.trim() ? ` an die Gruppe "${targetGroup.trim()}"` : ' an alle aktiven Benutzer'} senden moechten?
Sind Sie sicher, dass Sie diese Benachrichtigung an {filterDescription} senden moechten?
{previewCount !== null && (
<> ({previewCount} {previewCount === 1 ? 'Empfaenger' : 'Empfaenger'})</>
)}
</DialogContentText>
</DialogContent>
<DialogActions>