calendar download, date input validate, nc talk notification

This commit is contained in:
Matthias Hochmeister
2026-03-04 15:01:26 +01:00
parent d27d2931a5
commit fb5acd3d52
4 changed files with 86 additions and 9 deletions

View File

@@ -102,7 +102,11 @@ const NotificationBell: React.FC = () => {
} }
handleClose(); handleClose();
if (n.link) { if (n.link) {
navigate(n.link); if (n.link.startsWith('http://') || n.link.startsWith('https://')) {
window.open(n.link, '_blank');
} else {
navigate(n.link);
}
} }
}; };

View File

@@ -45,7 +45,7 @@ import { atemschutzApi } from '../services/atemschutz';
import { membersService } from '../services/members'; import { membersService } from '../services/members';
import { useNotification } from '../contexts/NotificationContext'; import { useNotification } from '../contexts/NotificationContext';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { toGermanDate, fromGermanDate } from '../utils/dateInput'; import { toGermanDate, fromGermanDate, isValidGermanDate } from '../utils/dateInput';
import type { import type {
AtemschutzUebersicht, AtemschutzUebersicht,
AtemschutzStats, AtemschutzStats,
@@ -162,6 +162,7 @@ function Atemschutz() {
const [form, setForm] = useState<AtemschutzFormState>({ ...EMPTY_FORM }); const [form, setForm] = useState<AtemschutzFormState>({ ...EMPTY_FORM });
const [dialogLoading, setDialogLoading] = useState(false); const [dialogLoading, setDialogLoading] = useState(false);
const [dialogError, setDialogError] = useState<string | null>(null); const [dialogError, setDialogError] = useState<string | null>(null);
const [dateErrors, setDateErrors] = useState<Partial<Record<keyof AtemschutzFormState, string>>>({});
// Delete confirmation // Delete confirmation
const [deleteId, setDeleteId] = useState<string | null>(null); const [deleteId, setDeleteId] = useState<string | null>(null);
@@ -248,6 +249,7 @@ function Atemschutz() {
setEditingId(null); setEditingId(null);
setForm({ ...EMPTY_FORM }); setForm({ ...EMPTY_FORM });
setDialogError(null); setDialogError(null);
setDateErrors({});
}; };
const handleFormChange = ( const handleFormChange = (
@@ -266,12 +268,46 @@ function Atemschutz() {
const handleSubmit = async () => { const handleSubmit = async () => {
setDialogError(null); setDialogError(null);
setDateErrors({});
if (!editingId && !form.user_id) { if (!editingId && !form.user_id) {
setDialogError('Bitte ein Mitglied auswählen.'); setDialogError('Bitte ein Mitglied auswählen.');
return; return;
} }
// Validate date fields
const newDateErrors: Partial<Record<keyof AtemschutzFormState, string>> = {};
const dateFields: (keyof AtemschutzFormState)[] = [
'lehrgang_datum', 'untersuchung_datum', 'untersuchung_gueltig_bis',
'leistungstest_datum', 'leistungstest_gueltig_bis',
];
for (const field of dateFields) {
const val = form[field] as string;
if (val && !isValidGermanDate(val)) {
newDateErrors[field] = 'Ungültiges Datum (Format: TT.MM.JJJJ)';
}
}
if (form.untersuchung_datum && form.untersuchung_gueltig_bis &&
isValidGermanDate(form.untersuchung_datum) && isValidGermanDate(form.untersuchung_gueltig_bis)) {
const from = new Date(fromGermanDate(form.untersuchung_datum)!);
const to = new Date(fromGermanDate(form.untersuchung_gueltig_bis)!);
if (to < from) {
newDateErrors['untersuchung_gueltig_bis'] = 'Muss nach dem Untersuchungsdatum liegen';
}
}
if (form.leistungstest_datum && form.leistungstest_gueltig_bis &&
isValidGermanDate(form.leistungstest_datum) && isValidGermanDate(form.leistungstest_gueltig_bis)) {
const from = new Date(fromGermanDate(form.leistungstest_datum)!);
const to = new Date(fromGermanDate(form.leistungstest_gueltig_bis)!);
if (to < from) {
newDateErrors['leistungstest_gueltig_bis'] = 'Muss nach dem Leistungstestdatum liegen';
}
}
if (Object.keys(newDateErrors).length > 0) {
setDateErrors(newDateErrors);
return;
}
setDialogLoading(true); setDialogLoading(true);
try { try {
if (editingId) { if (editingId) {
@@ -653,7 +689,8 @@ function Atemschutz() {
placeholder="TT.MM.JJJJ" placeholder="TT.MM.JJJJ"
value={form.lehrgang_datum} value={form.lehrgang_datum}
onChange={(e) => handleFormChange('lehrgang_datum', e.target.value)} onChange={(e) => handleFormChange('lehrgang_datum', e.target.value)}
helperText="Format: 01.03.2025" error={!!dateErrors.lehrgang_datum}
helperText={dateErrors.lehrgang_datum ?? 'Format: 01.03.2025'}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
/> />
</Grid> </Grid>
@@ -672,7 +709,8 @@ function Atemschutz() {
placeholder="TT.MM.JJJJ" placeholder="TT.MM.JJJJ"
value={form.untersuchung_datum} value={form.untersuchung_datum}
onChange={(e) => handleFormChange('untersuchung_datum', e.target.value)} onChange={(e) => handleFormChange('untersuchung_datum', e.target.value)}
helperText="Format: 08.02.2023" error={!!dateErrors.untersuchung_datum}
helperText={dateErrors.untersuchung_datum ?? 'Format: 08.02.2023'}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
/> />
</Grid> </Grid>
@@ -684,7 +722,8 @@ function Atemschutz() {
placeholder="TT.MM.JJJJ" placeholder="TT.MM.JJJJ"
value={form.untersuchung_gueltig_bis} value={form.untersuchung_gueltig_bis}
onChange={(e) => handleFormChange('untersuchung_gueltig_bis', e.target.value)} onChange={(e) => handleFormChange('untersuchung_gueltig_bis', e.target.value)}
helperText="Format: 08.02.2028" error={!!dateErrors.untersuchung_gueltig_bis}
helperText={dateErrors.untersuchung_gueltig_bis ?? 'Format: 08.02.2028'}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
/> />
</Grid> </Grid>
@@ -722,7 +761,8 @@ function Atemschutz() {
placeholder="TT.MM.JJJJ" placeholder="TT.MM.JJJJ"
value={form.leistungstest_datum} value={form.leistungstest_datum}
onChange={(e) => handleFormChange('leistungstest_datum', e.target.value)} onChange={(e) => handleFormChange('leistungstest_datum', e.target.value)}
helperText="Format: 25.08.2025" error={!!dateErrors.leistungstest_datum}
helperText={dateErrors.leistungstest_datum ?? 'Format: 25.08.2025'}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
/> />
</Grid> </Grid>
@@ -734,7 +774,8 @@ function Atemschutz() {
placeholder="TT.MM.JJJJ" placeholder="TT.MM.JJJJ"
value={form.leistungstest_gueltig_bis} value={form.leistungstest_gueltig_bis}
onChange={(e) => handleFormChange('leistungstest_gueltig_bis', e.target.value)} onChange={(e) => handleFormChange('leistungstest_gueltig_bis', e.target.value)}
helperText="Format: 25.08.2026" error={!!dateErrors.leistungstest_gueltig_bis}
helperText={dateErrors.leistungstest_gueltig_bis ?? 'Format: 25.08.2026'}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
/> />
</Grid> </Grid>

View File

@@ -1807,6 +1807,20 @@ export default function Kalender() {
}, [bookingForm.fahrzeugId, bookingForm.beginn, bookingForm.ende]); }, [bookingForm.fahrzeugId, bookingForm.beginn, bookingForm.ende]);
const handleBookingSave = async () => { const handleBookingSave = async () => {
if (!isValidGermanDateTime(bookingForm.beginn)) {
setBookingDialogError('Ungültiges Beginn-Datum (Format: TT.MM.JJJJ HH:MM)');
return;
}
if (!isValidGermanDateTime(bookingForm.ende)) {
setBookingDialogError('Ungültiges Ende-Datum (Format: TT.MM.JJJJ HH:MM)');
return;
}
const beginnIso = fromGermanDateTime(bookingForm.beginn)!;
const endeIso = fromGermanDateTime(bookingForm.ende)!;
if (new Date(endeIso) <= new Date(beginnIso)) {
setBookingDialogError('Ende muss nach dem Beginn liegen');
return;
}
setBookingDialogLoading(true); setBookingDialogLoading(true);
setBookingDialogError(null); setBookingDialogError(null);
try { try {
@@ -2216,7 +2230,9 @@ export default function Kalender() {
<Button <Button
variant="outlined" variant="outlined"
startIcon={<FileDownloadIcon />} startIcon={<FileDownloadIcon />}
onClick={() => window.open(icalEventUrl, '_blank')} component="a"
href={icalEventUrl}
download="veranstaltungen.ics"
> >
Herunterladen Herunterladen
</Button> </Button>
@@ -2711,7 +2727,9 @@ export default function Kalender() {
<Button <Button
variant="outlined" variant="outlined"
startIcon={<FileDownloadIcon />} startIcon={<FileDownloadIcon />}
onClick={() => window.open(icalBookingUrl, '_blank')} component="a"
href={icalBookingUrl}
download="buchungen.ics"
> >
Herunterladen Herunterladen
</Button> </Button>

View File

@@ -642,6 +642,20 @@ function EventFormDialog({
notification.showError('Titel ist erforderlich'); notification.showError('Titel ist erforderlich');
return; return;
} }
const vonDate = new Date(form.datum_von);
const bisDate = new Date(form.datum_bis);
if (isNaN(vonDate.getTime())) {
notification.showError('Ungültiges Von-Datum');
return;
}
if (isNaN(bisDate.getTime())) {
notification.showError('Ungültiges Bis-Datum');
return;
}
if (bisDate < vonDate) {
notification.showError('Bis-Datum muss nach dem Von-Datum liegen');
return;
}
setLoading(true); setLoading(true);
try { try {
if (editingEvent) { if (editingEvent) {