new features

This commit is contained in:
Matthias Hochmeister
2026-03-23 14:01:39 +01:00
parent d2dc64d54a
commit 3326156b15
35 changed files with 1341 additions and 257 deletions

View File

@@ -21,8 +21,6 @@ import {
InputLabel,
FormControlLabel,
Switch,
Checkbox,
FormGroup,
Stack,
List,
ListItem,
@@ -34,6 +32,9 @@ import {
useTheme,
useMediaQuery,
Snackbar,
Autocomplete,
Radio,
RadioGroup,
} from '@mui/material';
import {
Add,
@@ -61,6 +62,7 @@ import type {
GroupInfo,
CreateVeranstaltungInput,
ConflictEvent,
WiederholungConfig,
} from '../types/events.types';
// ---------------------------------------------------------------------------
@@ -667,16 +669,6 @@ function EventFormDialog({
setForm((prev) => ({ ...prev, [field]: value }));
};
const handleGroupToggle = (groupId: string) => {
setForm((prev) => {
const current = prev.zielgruppen;
const updated = current.includes(groupId)
? current.filter((g) => g !== groupId)
: [...current, groupId];
return { ...prev, zielgruppen: updated };
});
};
const handleSave = async () => {
if (!form.titel.trim()) {
notification.showError('Titel ist erforderlich');
@@ -866,28 +858,33 @@ function EventFormDialog({
label="Für alle Mitglieder sichtbar"
/>
{/* Zielgruppen checkboxes */}
{/* Zielgruppen multi-select */}
{!form.alle_gruppen && groups.length > 0 && (
<Box>
<Typography variant="body2" fontWeight={600} sx={{ mb: 0.5 }}>
Zielgruppen
</Typography>
<FormGroup>
{groups.map((g) => (
<FormControlLabel
key={g.id}
control={
<Checkbox
checked={form.zielgruppen.includes(g.id)}
onChange={() => handleGroupToggle(g.id)}
size="small"
/>
}
label={g.label}
<Autocomplete
multiple
options={groups}
getOptionLabel={(option) => option.label}
value={groups.filter((g) => form.zielgruppen.includes(g.id))}
onChange={(_, newValue) => {
handleChange('zielgruppen', newValue.map((g) => g.id));
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderInput={(params) => (
<TextField {...params} label="Zielgruppen" placeholder="Gruppen auswählen" />
)}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
{...getTagProps({ index })}
key={option.id}
label={option.label}
size="small"
/>
))}
</FormGroup>
</Box>
))
}
size="small"
disableCloseOnSelect
/>
)}
<Divider />
@@ -929,6 +926,103 @@ function EventFormDialog({
inputProps={{ min: 1 }}
fullWidth
/>
{/* Recurrence / Wiederholung — only for new events */}
{!editingEvent && (
<>
<Divider />
<FormControlLabel
control={
<Switch
checked={Boolean(form.wiederholung)}
onChange={(e) => {
if (e.target.checked) {
const bisDefault = new Date(form.datum_von);
bisDefault.setMonth(bisDefault.getMonth() + 3);
handleChange('wiederholung', {
typ: 'wöchentlich',
intervall: 1,
bis: bisDefault.toISOString().slice(0, 10),
} as WiederholungConfig);
} else {
handleChange('wiederholung', null);
}
}}
/>
}
label="Wiederholung"
/>
{form.wiederholung && (
<Stack spacing={2} sx={{ pl: 2 }}>
<FormControl fullWidth size="small">
<InputLabel>Häufigkeit</InputLabel>
<Select
label="Häufigkeit"
value={form.wiederholung.typ}
onChange={(e) => {
const w = { ...form.wiederholung!, typ: e.target.value as WiederholungConfig['typ'] };
handleChange('wiederholung', w);
}}
>
<MenuItem value="wöchentlich">Wöchentlich</MenuItem>
<MenuItem value="zweiwöchentlich">Zweiwöchentlich</MenuItem>
<MenuItem value="monatlich_datum">Monatlich (gleicher Tag)</MenuItem>
<MenuItem value="monatlich_erster_wochentag">Monatlich (erster Wochentag)</MenuItem>
<MenuItem value="monatlich_letzter_wochentag">Monatlich (letzter Wochentag)</MenuItem>
</Select>
</FormControl>
{form.wiederholung.typ === 'wöchentlich' && (
<TextField
label="Alle X Wochen"
type="number"
size="small"
value={form.wiederholung.intervall ?? 1}
onChange={(e) => {
const w = { ...form.wiederholung!, intervall: Math.max(1, Number(e.target.value) || 1) };
handleChange('wiederholung', w);
}}
inputProps={{ min: 1, max: 52 }}
fullWidth
/>
)}
{(form.wiederholung.typ === 'monatlich_erster_wochentag' ||
form.wiederholung.typ === 'monatlich_letzter_wochentag') && (
<FormControl fullWidth size="small">
<InputLabel>Wochentag</InputLabel>
<Select
label="Wochentag"
value={form.wiederholung.wochentag ?? 0}
onChange={(e) => {
const w = { ...form.wiederholung!, wochentag: Number(e.target.value) };
handleChange('wiederholung', w);
}}
>
{WEEKDAY_LABELS.map((label, idx) => (
<MenuItem key={idx} value={idx}>{label === 'Mo' ? 'Montag' : label === 'Di' ? 'Dienstag' : label === 'Mi' ? 'Mittwoch' : label === 'Do' ? 'Donnerstag' : label === 'Fr' ? 'Freitag' : label === 'Sa' ? 'Samstag' : 'Sonntag'}</MenuItem>
))}
</Select>
</FormControl>
)}
<TextField
label="Wiederholen bis"
type="date"
size="small"
value={form.wiederholung.bis}
onChange={(e) => {
const w = { ...form.wiederholung!, bis: e.target.value };
handleChange('wiederholung', w);
}}
InputLabelProps={{ shrink: true }}
fullWidth
helperText="Enddatum der Wiederholungsserie"
/>
</Stack>
)}
</>
)}
</Stack>
</DialogContent>
<DialogActions>
@@ -1105,6 +1199,7 @@ export default function Veranstaltungen() {
// Delete dialog
const [deleteId, setDeleteId] = useState<string | null>(null);
const [deleteLoading, setDeleteLoading] = useState(false);
const [deleteMode, setDeleteMode] = useState<'all' | 'single' | 'future'>('all');
// iCal dialog
const [icalOpen, setIcalOpen] = useState(false);
@@ -1215,8 +1310,9 @@ export default function Veranstaltungen() {
if (!deleteId) return;
setDeleteLoading(true);
try {
await eventsApi.deleteEvent(deleteId);
await eventsApi.deleteEvent(deleteId, deleteMode);
setDeleteId(null);
setDeleteMode('all');
loadData();
notification.showSuccess('Veranstaltung wurde gelöscht');
} catch (e: unknown) {
@@ -1373,7 +1469,12 @@ export default function Veranstaltungen() {
canWrite={canWrite}
onEdit={(ev) => { setEditingEvent(ev); setFormOpen(true); }}
onCancel={(id) => { setCancelId(id); setCancelGrund(''); }}
onDelete={(id) => setDeleteId(id)}
onDelete={(id) => {
const ev = events.find((e) => e.id === id);
const isRecurring = ev && (ev.wiederholung_parent_id || ev.wiederholung);
setDeleteMode(isRecurring ? 'single' : 'all');
setDeleteId(id);
}}
/>
</Paper>
)}
@@ -1444,15 +1545,38 @@ export default function Veranstaltungen() {
</Dialog>
{/* Delete Dialog */}
<Dialog open={Boolean(deleteId)} onClose={() => setDeleteId(null)} maxWidth="xs" fullWidth>
<Dialog open={Boolean(deleteId)} onClose={() => { setDeleteId(null); setDeleteMode('all'); }} 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>
{(() => {
const deleteEvent = events.find((ev) => ev.id === deleteId);
const isRecurring = deleteEvent && (deleteEvent.wiederholung_parent_id || deleteEvent.wiederholung);
if (isRecurring) {
return (
<>
<DialogContentText sx={{ mb: 2 }}>
Diese Veranstaltung ist Teil einer Wiederholungsserie. Was soll gelöscht werden?
</DialogContentText>
<RadioGroup
value={deleteMode}
onChange={(e) => setDeleteMode(e.target.value as 'all' | 'single' | 'future')}
>
<FormControlLabel value="single" control={<Radio />} label="Nur diesen Termin" />
<FormControlLabel value="future" control={<Radio />} label="Diesen und alle folgenden Termine" />
<FormControlLabel value="all" control={<Radio />} label="Alle Termine der Serie" />
</RadioGroup>
</>
);
}
return (
<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 onClick={() => { setDeleteId(null); setDeleteMode('all'); }}>Abbrechen</Button>
<Button variant="contained" color="error" onClick={handleDeleteEvent} disabled={deleteLoading}>
{deleteLoading ? <CircularProgress size={20} /> : 'Endgültig löschen'}
</Button>