new features
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user