new features, bookstack
This commit is contained in:
@@ -55,8 +55,11 @@ import {
|
||||
DirectionsCar as CarIcon,
|
||||
Edit as EditIcon,
|
||||
Event as EventIcon,
|
||||
FileDownload as FileDownloadIcon,
|
||||
FileUpload as FileUploadIcon,
|
||||
HelpOutline as UnknownIcon,
|
||||
IosShare,
|
||||
PictureAsPdf as PdfIcon,
|
||||
Star as StarIcon,
|
||||
Today as TodayIcon,
|
||||
Tune,
|
||||
@@ -65,6 +68,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { toGermanDate, toGermanDateTime, fromGermanDate, fromGermanDateTime } from '../utils/dateInput';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import { trainingApi } from '../services/training';
|
||||
@@ -193,13 +197,12 @@ function formatDateLong(d: Date): string {
|
||||
return `${days[d.getDay()]}, ${d.getDate()}. ${MONTH_LABELS[d.getMonth()]} ${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
function toDatetimeLocal(isoString: string): string {
|
||||
const d = new Date(isoString);
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
function fromDatetimeLocal(value: string): string {
|
||||
if (!value) return new Date().toISOString();
|
||||
const dtIso = fromGermanDateTime(value);
|
||||
if (dtIso) return new Date(dtIso).toISOString();
|
||||
const dIso = fromGermanDate(value);
|
||||
if (dIso) return new Date(dIso).toISOString();
|
||||
return new Date(value).toISOString();
|
||||
}
|
||||
|
||||
@@ -609,6 +612,323 @@ function DayPopover({
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// PDF Export helper
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function generatePdf(
|
||||
year: number,
|
||||
month: number,
|
||||
trainingEvents: UebungListItem[],
|
||||
veranstaltungen: VeranstaltungListItem[],
|
||||
) {
|
||||
// Dynamically import jsPDF to avoid bundle bloat if not needed
|
||||
const { jsPDF } = await import('jspdf');
|
||||
const autoTable = (await import('jspdf-autotable')).default;
|
||||
|
||||
const doc = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' });
|
||||
const monthLabel = MONTH_LABELS[month];
|
||||
|
||||
// Header bar
|
||||
doc.setFillColor(183, 28, 28); // fire-red
|
||||
doc.rect(0, 0, 297, 18, 'F');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setFontSize(14);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(`Kalender — ${monthLabel} ${year}`, 10, 12);
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text('Feuerwehr Rems', 250, 12);
|
||||
|
||||
// Build combined list (same logic as CombinedListView)
|
||||
type ListEntry =
|
||||
| { kind: 'training'; item: UebungListItem }
|
||||
| { kind: 'event'; item: VeranstaltungListItem };
|
||||
|
||||
const combined: ListEntry[] = [
|
||||
...trainingEvents.map((t): ListEntry => ({ kind: 'training', item: t })),
|
||||
...veranstaltungen.map((e): ListEntry => ({ kind: 'event', item: e })),
|
||||
].sort((a, b) => a.item.datum_von.localeCompare(b.item.datum_von));
|
||||
|
||||
const formatDateCell = (iso: string) => {
|
||||
const d = new Date(iso);
|
||||
return `${String(d.getDate()).padStart(2, '0')}.${String(d.getMonth() + 1).padStart(2, '0')}.${d.getFullYear()}`;
|
||||
};
|
||||
const formatTimeCell = (iso: string) => {
|
||||
const d = new Date(iso);
|
||||
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const rows = combined.map((entry) => {
|
||||
const item = entry.item;
|
||||
return [
|
||||
formatDateCell(item.datum_von),
|
||||
formatTimeCell(item.datum_von),
|
||||
item.titel,
|
||||
entry.kind === 'training'
|
||||
? (item as UebungListItem).typ
|
||||
: ((item as VeranstaltungListItem).kategorie_name ?? 'Veranstaltung'),
|
||||
(item as any).ort ?? '',
|
||||
];
|
||||
});
|
||||
|
||||
autoTable(doc, {
|
||||
head: [['Datum', 'Uhrzeit', 'Titel', 'Kategorie/Typ', 'Ort']],
|
||||
body: rows,
|
||||
startY: 22,
|
||||
headStyles: { fillColor: [183, 28, 28], textColor: 255, fontStyle: 'bold' },
|
||||
alternateRowStyles: { fillColor: [250, 235, 235] },
|
||||
margin: { left: 10, right: 10 },
|
||||
styles: { fontSize: 9, cellPadding: 2 },
|
||||
columnStyles: {
|
||||
0: { cellWidth: 25 },
|
||||
1: { cellWidth: 18 },
|
||||
2: { cellWidth: 90 },
|
||||
3: { cellWidth: 40 },
|
||||
4: { cellWidth: 60 },
|
||||
},
|
||||
});
|
||||
|
||||
const filename = `kalender_${year}_${String(month + 1).padStart(2, '0')}.pdf`;
|
||||
doc.save(filename);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// CSV Import Dialog
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const CSV_EXAMPLE = [
|
||||
'Titel;Datum Von;Datum Bis;Ganztaegig;Ort;Kategorie;Beschreibung',
|
||||
'Übung Atemschutz;15.03.2026 19:00;15.03.2026 21:00;Nein;Feuerwehrhaus;Übung;Atemschutzübung für alle',
|
||||
'Tag der offenen Tür;20.04.2026;20.04.2026;Ja;Feuerwehrhaus;Veranstaltung;',
|
||||
].join('\n');
|
||||
|
||||
interface CsvRow {
|
||||
titel: string;
|
||||
datum_von: string;
|
||||
datum_bis: string;
|
||||
ganztaegig: boolean;
|
||||
ort: string | null;
|
||||
beschreibung: string | null;
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
function parseCsvRow(line: string, lineNo: number): CsvRow {
|
||||
const parts = line.split(';');
|
||||
if (parts.length < 4) {
|
||||
return { titel: '', datum_von: '', datum_bis: '', ganztaegig: false, ort: null, beschreibung: null, valid: false, error: `Zeile ${lineNo}: Zu wenige Spalten` };
|
||||
}
|
||||
const [titel, rawVon, rawBis, rawGanztaegig, ort, , beschreibung] = parts;
|
||||
const ganztaegig = rawGanztaegig?.trim().toLowerCase() === 'ja';
|
||||
|
||||
const convertDate = (raw: string): string => {
|
||||
const trimmed = raw.trim();
|
||||
// DD.MM.YYYY HH:MM
|
||||
const dtIso = fromGermanDateTime(trimmed);
|
||||
if (dtIso) return new Date(dtIso).toISOString();
|
||||
// DD.MM.YYYY
|
||||
const dIso = fromGermanDate(trimmed);
|
||||
if (dIso) return new Date(dIso + 'T00:00:00').toISOString();
|
||||
return '';
|
||||
};
|
||||
|
||||
const datum_von = convertDate(rawVon ?? '');
|
||||
const datum_bis = convertDate(rawBis ?? '');
|
||||
|
||||
if (!titel?.trim()) {
|
||||
return { titel: '', datum_von, datum_bis, ganztaegig, ort: null, beschreibung: null, valid: false, error: `Zeile ${lineNo}: Titel fehlt` };
|
||||
}
|
||||
if (!datum_von) {
|
||||
return { titel: titel.trim(), datum_von, datum_bis, ganztaegig, ort: null, beschreibung: null, valid: false, error: `Zeile ${lineNo}: Datum Von ungültig` };
|
||||
}
|
||||
|
||||
return {
|
||||
titel: titel.trim(),
|
||||
datum_von,
|
||||
datum_bis: datum_bis || datum_von,
|
||||
ganztaegig,
|
||||
ort: ort?.trim() || null,
|
||||
beschreibung: beschreibung?.trim() || null,
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
|
||||
interface CsvImportDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onImported: () => void;
|
||||
}
|
||||
|
||||
function CsvImportDialog({ open, onClose, onImported }: CsvImportDialogProps) {
|
||||
const [rows, setRows] = useState<CsvRow[]>([]);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [result, setResult] = useState<{ created: number; errors: string[] } | null>(null);
|
||||
const notification = useNotification();
|
||||
const fileRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
const text = ev.target?.result as string;
|
||||
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
||||
// Skip header line
|
||||
const dataLines = lines[0]?.toLowerCase().includes('titel') ? lines.slice(1) : lines;
|
||||
const parsed = dataLines.map((line, i) => parseCsvRow(line, i + 2));
|
||||
setRows(parsed);
|
||||
setResult(null);
|
||||
};
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
};
|
||||
|
||||
const downloadExample = () => {
|
||||
const blob = new Blob(['\uFEFF' + CSV_EXAMPLE], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'kalender_import_beispiel.csv';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const validRows = rows.filter((r) => r.valid);
|
||||
|
||||
const handleImport = async () => {
|
||||
if (validRows.length === 0) return;
|
||||
setImporting(true);
|
||||
try {
|
||||
const events = validRows.map((r) => ({
|
||||
titel: r.titel,
|
||||
datum_von: r.datum_von,
|
||||
datum_bis: r.datum_bis,
|
||||
ganztaegig: r.ganztaegig,
|
||||
ort: r.ort,
|
||||
beschreibung: r.beschreibung,
|
||||
zielgruppen: [],
|
||||
alle_gruppen: true,
|
||||
anmeldung_erforderlich: false,
|
||||
}));
|
||||
const res = await eventsApi.importEvents(events);
|
||||
setResult(res);
|
||||
if (res.created > 0) {
|
||||
notification.showSuccess(`${res.created} Veranstaltung${res.created !== 1 ? 'en' : ''} importiert`);
|
||||
onImported();
|
||||
}
|
||||
} catch {
|
||||
notification.showError('Import fehlgeschlagen');
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setRows([]);
|
||||
setResult(null);
|
||||
if (fileRef.current) fileRef.current.value = '';
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Kalender importieren (CSV)</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} sx={{ mt: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<FileDownloadIcon />}
|
||||
onClick={downloadExample}
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
Beispiel-CSV herunterladen
|
||||
</Button>
|
||||
|
||||
<Box>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept=".csv,text/csv"
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
id="csv-import-input"
|
||||
/>
|
||||
<label htmlFor="csv-import-input">
|
||||
<Button variant="contained" component="span" startIcon={<FileUploadIcon />}>
|
||||
CSV-Datei auswählen
|
||||
</Button>
|
||||
</label>
|
||||
</Box>
|
||||
|
||||
{rows.length > 0 && (
|
||||
<>
|
||||
<Typography variant="body2">
|
||||
{validRows.length} gültige / {rows.length - validRows.length} fehlerhafte Zeilen
|
||||
</Typography>
|
||||
{rows.some((r) => !r.valid) && (
|
||||
<Alert severity="warning" sx={{ whiteSpace: 'pre-line' }}>
|
||||
{rows.filter((r) => !r.valid).map((r) => r.error).join('\n')}
|
||||
</Alert>
|
||||
)}
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: 300 }}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Titel</TableCell>
|
||||
<TableCell>Von</TableCell>
|
||||
<TableCell>Bis</TableCell>
|
||||
<TableCell>Ganztägig</TableCell>
|
||||
<TableCell>Ort</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, i) => (
|
||||
<TableRow key={i} sx={{ bgcolor: row.valid ? undefined : 'error.light' }}>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={row.valid ? 'OK' : 'Fehler'}
|
||||
color={row.valid ? 'success' : 'error'}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{row.titel}</TableCell>
|
||||
<TableCell>{row.datum_von ? new Date(row.datum_von).toLocaleDateString('de-DE') : '—'}</TableCell>
|
||||
<TableCell>{row.datum_bis ? new Date(row.datum_bis).toLocaleDateString('de-DE') : '—'}</TableCell>
|
||||
<TableCell>{row.ganztaegig ? 'Ja' : 'Nein'}</TableCell>
|
||||
<TableCell>{row.ort ?? '—'}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<Alert severity={result.errors.length === 0 ? 'success' : 'warning'}>
|
||||
{result.created} Veranstaltung{result.created !== 1 ? 'en' : ''} importiert.
|
||||
{result.errors.length > 0 && ` ${result.errors.length} Fehler.`}
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Schließen</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleImport}
|
||||
disabled={validRows.length === 0 || importing}
|
||||
startIcon={importing ? <CircularProgress size={16} /> : <FileUploadIcon />}
|
||||
>
|
||||
{validRows.length > 0 ? `${validRows.length} importieren` : 'Importieren'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Combined List View (training + events sorted by date)
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -874,7 +1194,7 @@ function VeranstaltungFormDialog({
|
||||
wiederholung: (!editingEvent && wiederholungAktiv && wiederholungBis)
|
||||
? {
|
||||
typ: wiederholungTyp,
|
||||
bis: wiederholungBis,
|
||||
bis: fromGermanDate(wiederholungBis) || wiederholungBis,
|
||||
intervall: wiederholungTyp === 'wöchentlich' ? wiederholungIntervall : undefined,
|
||||
wochentag: (wiederholungTyp === 'monatlich_erster_wochentag' || wiederholungTyp === 'monatlich_letzter_wochentag')
|
||||
? wiederholungWochentag
|
||||
@@ -956,16 +1276,17 @@ function VeranstaltungFormDialog({
|
||||
/>
|
||||
<TextField
|
||||
label="Von"
|
||||
type={form.ganztaegig ? 'date' : 'datetime-local'}
|
||||
placeholder={form.ganztaegig ? 'TT.MM.JJJJ' : 'TT.MM.JJJJ HH:MM'}
|
||||
value={
|
||||
form.ganztaegig
|
||||
? toDatetimeLocal(form.datum_von).slice(0, 10)
|
||||
: toDatetimeLocal(form.datum_von)
|
||||
? toGermanDate(form.datum_von)
|
||||
: toGermanDateTime(form.datum_von)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
const iso = form.ganztaegig
|
||||
? fromDatetimeLocal(`${e.target.value}T00:00`)
|
||||
: fromDatetimeLocal(e.target.value);
|
||||
? fromDatetimeLocal(raw ? `${fromGermanDate(raw)} 00:00` : '')
|
||||
: fromDatetimeLocal(raw);
|
||||
handleChange('datum_von', iso);
|
||||
}}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
@@ -973,16 +1294,17 @@ function VeranstaltungFormDialog({
|
||||
/>
|
||||
<TextField
|
||||
label="Bis"
|
||||
type={form.ganztaegig ? 'date' : 'datetime-local'}
|
||||
placeholder={form.ganztaegig ? 'TT.MM.JJJJ' : 'TT.MM.JJJJ HH:MM'}
|
||||
value={
|
||||
form.ganztaegig
|
||||
? toDatetimeLocal(form.datum_bis).slice(0, 10)
|
||||
: toDatetimeLocal(form.datum_bis)
|
||||
? toGermanDate(form.datum_bis)
|
||||
: toGermanDateTime(form.datum_bis)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
const iso = form.ganztaegig
|
||||
? fromDatetimeLocal(`${e.target.value}T23:59`)
|
||||
: fromDatetimeLocal(e.target.value);
|
||||
? fromDatetimeLocal(raw ? `${fromGermanDate(raw)} 23:59` : '')
|
||||
: fromDatetimeLocal(raw);
|
||||
handleChange('datum_bis', iso);
|
||||
}}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
@@ -1039,11 +1361,14 @@ function VeranstaltungFormDialog({
|
||||
{form.anmeldung_erforderlich && (
|
||||
<TextField
|
||||
label="Anmeldeschluss"
|
||||
type="datetime-local"
|
||||
value={form.anmeldung_bis ? toDatetimeLocal(form.anmeldung_bis) : ''}
|
||||
onChange={(e) =>
|
||||
handleChange('anmeldung_bis', e.target.value ? fromDatetimeLocal(e.target.value) : null)
|
||||
}
|
||||
placeholder="TT.MM.JJJJ HH:MM"
|
||||
value={form.anmeldung_bis ? toGermanDateTime(form.anmeldung_bis) : ''}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
if (!raw) { handleChange('anmeldung_bis', null); return; }
|
||||
const iso = fromGermanDateTime(raw);
|
||||
if (iso) handleChange('anmeldung_bis', new Date(iso).toISOString());
|
||||
}}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
fullWidth
|
||||
/>
|
||||
@@ -1109,8 +1434,8 @@ function VeranstaltungFormDialog({
|
||||
|
||||
<TextField
|
||||
label="Wiederholungen bis"
|
||||
type="date"
|
||||
size="small"
|
||||
placeholder="TT.MM.JJJJ"
|
||||
value={wiederholungBis}
|
||||
onChange={(e) => setWiederholungBis(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
@@ -1216,6 +1541,7 @@ export default function Kalender() {
|
||||
const [icalEventOpen, setIcalEventOpen] = useState(false);
|
||||
const [icalEventUrl, setIcalEventUrl] = useState('');
|
||||
const [icalBookingOpen, setIcalBookingOpen] = useState(false);
|
||||
const [csvImportOpen, setCsvImportOpen] = useState(false);
|
||||
const [icalBookingUrl, setIcalBookingUrl] = useState('');
|
||||
|
||||
// ── Data loading ─────────────────────────────────────────────────────────────
|
||||
@@ -1405,8 +1731,8 @@ export default function Kalender() {
|
||||
setBookingForm({
|
||||
...EMPTY_BOOKING_FORM,
|
||||
fahrzeugId: vehicleId,
|
||||
beginn: fnsFormat(day, "yyyy-MM-dd'T'08:00"),
|
||||
ende: fnsFormat(day, "yyyy-MM-dd'T'17:00"),
|
||||
beginn: toGermanDateTime(fnsFormat(day, "yyyy-MM-dd'T'08:00")),
|
||||
ende: toGermanDateTime(fnsFormat(day, "yyyy-MM-dd'T'17:00")),
|
||||
});
|
||||
setBookingDialogError(null);
|
||||
setAvailability(null);
|
||||
@@ -1420,8 +1746,8 @@ export default function Kalender() {
|
||||
fahrzeugId: detailBooking.fahrzeug_id,
|
||||
titel: detailBooking.titel,
|
||||
beschreibung: '',
|
||||
beginn: fnsFormat(parseISO(detailBooking.beginn as unknown as string), "yyyy-MM-dd'T'HH:mm"),
|
||||
ende: fnsFormat(parseISO(detailBooking.ende as unknown as string), "yyyy-MM-dd'T'HH:mm"),
|
||||
beginn: toGermanDateTime(fnsFormat(parseISO(detailBooking.beginn as unknown as string), "yyyy-MM-dd'T'HH:mm")),
|
||||
ende: toGermanDateTime(fnsFormat(parseISO(detailBooking.ende as unknown as string), "yyyy-MM-dd'T'HH:mm")),
|
||||
buchungsArt: detailBooking.buchungs_art,
|
||||
kontaktPerson: '',
|
||||
kontaktTelefon: '',
|
||||
@@ -1440,11 +1766,13 @@ export default function Kalender() {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const beginnIso = fromGermanDateTime(bookingForm.beginn) || bookingForm.beginn;
|
||||
const endeIso = fromGermanDateTime(bookingForm.ende) || bookingForm.ende;
|
||||
bookingApi
|
||||
.checkAvailability(
|
||||
bookingForm.fahrzeugId,
|
||||
new Date(bookingForm.beginn),
|
||||
new Date(bookingForm.ende)
|
||||
new Date(beginnIso),
|
||||
new Date(endeIso)
|
||||
)
|
||||
.then(({ available }) => {
|
||||
if (!cancelled) setAvailability(available);
|
||||
@@ -1461,8 +1789,8 @@ export default function Kalender() {
|
||||
try {
|
||||
const payload: CreateBuchungInput = {
|
||||
...bookingForm,
|
||||
beginn: new Date(bookingForm.beginn).toISOString(),
|
||||
ende: new Date(bookingForm.ende).toISOString(),
|
||||
beginn: (() => { const iso = fromGermanDateTime(bookingForm.beginn); return iso ? new Date(iso).toISOString() : new Date(bookingForm.beginn).toISOString(); })(),
|
||||
ende: (() => { const iso = fromGermanDateTime(bookingForm.ende); return iso ? new Date(iso).toISOString() : new Date(bookingForm.ende).toISOString(); })(),
|
||||
};
|
||||
if (editingBooking) {
|
||||
await bookingApi.update(editingBooking.id, payload);
|
||||
@@ -1622,6 +1950,35 @@ export default function Kalender() {
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* PDF Export — only in list view */}
|
||||
{viewMode === 'list' && (
|
||||
<Tooltip title="Als PDF exportieren">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => generatePdf(
|
||||
viewMonth.year,
|
||||
viewMonth.month,
|
||||
trainingForMonth,
|
||||
eventsForMonth,
|
||||
)}
|
||||
>
|
||||
<PdfIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* CSV Import */}
|
||||
{canWriteEvents && (
|
||||
<Tooltip title="Kalender importieren (CSV)">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setCsvImportOpen(true)}
|
||||
>
|
||||
<FileUploadIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* iCal subscribe */}
|
||||
<Button
|
||||
startIcon={<IosShare />}
|
||||
@@ -2166,7 +2523,7 @@ export default function Kalender() {
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth size="small" label="Beginn" type="datetime-local" required
|
||||
fullWidth size="small" label="Beginn" placeholder="TT.MM.JJJJ HH:MM" required
|
||||
value={bookingForm.beginn}
|
||||
onChange={(e) =>
|
||||
setBookingForm((f) => ({ ...f, beginn: e.target.value }))
|
||||
@@ -2175,7 +2532,7 @@ export default function Kalender() {
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth size="small" label="Ende" type="datetime-local" required
|
||||
fullWidth size="small" label="Ende" placeholder="TT.MM.JJJJ HH:MM" required
|
||||
value={bookingForm.ende}
|
||||
onChange={(e) =>
|
||||
setBookingForm((f) => ({ ...f, ende: e.target.value }))
|
||||
@@ -2327,6 +2684,16 @@ export default function Kalender() {
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
{/* CSV Import Dialog */}
|
||||
<CsvImportDialog
|
||||
open={csvImportOpen}
|
||||
onClose={() => setCsvImportOpen(false)}
|
||||
onImported={() => {
|
||||
setCsvImportOpen(false);
|
||||
loadCalendarData();
|
||||
}}
|
||||
/>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user