import { useState } from 'react'; import { Box, Container, Typography, Paper, Card, CardContent, Button, IconButton, TextField, Select, MenuItem, FormControl, InputLabel, CircularProgress, Alert, Chip, Dialog, DialogTitle, DialogContent, DialogActions, Tab, Tabs, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Checkbox, Stack, Tooltip, } from '@mui/material'; import { Add, IosShare, ContentCopy, Cancel, Edit, Delete, Save, Close, EventBusy, } from '@mui/icons-material'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { format, parseISO } from 'date-fns'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import ChatAwareFab from '../components/shared/ChatAwareFab'; import GermanDateField from '../components/shared/GermanDateField'; import ServiceModePage from '../components/shared/ServiceModePage'; import { useAuth } from '../contexts/AuthContext'; import { usePermissionContext } from '../contexts/PermissionContext'; import { useNotification } from '../contexts/NotificationContext'; import { bookingApi, fetchVehicles, kategorieApi } from '../services/bookings'; import type { BuchungsArt, BuchungsKategorie, } from '../types/booking.types'; import { BUCHUNGS_ART_LABELS, BUCHUNGS_ART_COLORS } from '../types/booking.types'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function formatBookingDate(beginn: string, ende: string, ganztaegig: boolean): string { try { const b = parseISO(beginn); const e = parseISO(ende); if (ganztaegig) { return `${format(b, 'dd.MM.yyyy')} – ${format(e, 'dd.MM.yyyy')} (Ganztägig)`; } return `${format(b, 'dd.MM.yyyy HH:mm')} – ${format(e, 'dd.MM.yyyy HH:mm')}`; } catch { return `${beginn} – ${ende}`; } } function getCategoryColor(buchungsArt: BuchungsArt, kategorien: BuchungsKategorie[]): string { const kat = kategorien.find((k) => k.bezeichnung.toLowerCase() === buchungsArt); if (kat) return kat.farbe; return BUCHUNGS_ART_COLORS[buchungsArt] || '#757575'; } function getCategoryLabel(buchungsArt: BuchungsArt, kategorien: BuchungsKategorie[]): string { const kat = kategorien.find((k) => k.bezeichnung.toLowerCase() === buchungsArt); if (kat) return kat.bezeichnung; return BUCHUNGS_ART_LABELS[buchungsArt] || buchungsArt; } // --------------------------------------------------------------------------- // Main Page // --------------------------------------------------------------------------- function FahrzeugBuchungen() { const { user } = useAuth(); const { hasPermission, isFeatureEnabled } = usePermissionContext(); const notification = useNotification(); const navigate = useNavigate(); const queryClient = useQueryClient(); const canCreate = hasPermission('fahrzeugbuchungen:create'); const canManage = hasPermission('fahrzeugbuchungen:manage'); const [tabIndex, setTabIndex] = useState(0); // ── Filters ──────────────────────────────────────────────────────────────── const today = new Date(); const defaultFrom = format(today, 'yyyy-MM-dd'); const defaultTo = format(new Date(today.getTime() + 90 * 86400000), 'yyyy-MM-dd'); const [filterFrom, setFilterFrom] = useState(defaultFrom); const [filterTo, setFilterTo] = useState(defaultTo); const [filterVehicle, setFilterVehicle] = useState(''); const [filterKategorie, setFilterKategorie] = useState(''); // ── Data ─────────────────────────────────────────────────────────────────── const { data: vehicles = [] } = useQuery({ queryKey: ['vehicles'], queryFn: fetchVehicles, }); const { data: kategorien = [] } = useQuery({ queryKey: ['buchungskategorien-all'], queryFn: kategorieApi.getAll, }); const { data: activeKategorien = [] } = useQuery({ queryKey: ['buchungskategorien'], queryFn: kategorieApi.getActive, }); const fromDate = filterFrom ? new Date(`${filterFrom}T00:00:00`) : new Date(); const toDate = filterTo ? new Date(`${filterTo}T23:59:59`) : new Date(Date.now() + 90 * 86400000); const { data: calendarData, isLoading, isError, error: loadError, } = useQuery({ queryKey: ['bookings-range', filterFrom, filterTo, filterVehicle], queryFn: () => bookingApi.getCalendarRange( fromDate, toDate, filterVehicle || undefined ), }); const bookings = (calendarData?.bookings || []) .filter((b) => !b.abgesagt) .filter((b) => !filterKategorie || b.buchungs_art === filterKategorie) .sort((a, b) => new Date(a.beginn).getTime() - new Date(b.beginn).getTime()); // ── Cancel ───────────────────────────────────────────────────────────────── const [cancelId, setCancelId] = useState(null); const [cancelGrund, setCancelGrund] = useState(''); const [cancelLoading, setCancelLoading] = useState(false); const handleCancel = async () => { if (!cancelId) return; setCancelLoading(true); try { await bookingApi.cancel(cancelId, cancelGrund); notification.showSuccess('Buchung storniert'); setCancelId(null); setCancelGrund(''); queryClient.invalidateQueries({ queryKey: ['bookings-range'] }); } catch (e: unknown) { const axiosErr = e as { response?: { data?: { message?: string } }; message?: string }; notification.showError( axiosErr?.response?.data?.message || (e instanceof Error ? e.message : 'Fehler beim Stornieren') ); } finally { setCancelLoading(false); } }; // ── iCal ─────────────────────────────────────────────────────────────────── const [icalOpen, setIcalOpen] = useState(false); const [icalUrl, setIcalUrl] = useState(''); const handleIcalOpen = async () => { try { const { subscribeUrl } = await bookingApi.getCalendarToken(); setIcalUrl(subscribeUrl); setIcalOpen(true); } catch (e: unknown) { notification.showError(e instanceof Error ? e.message : 'Fehler beim Laden des Tokens'); } }; // ── Einstellungen: Categories management ─────────────────────────────────── const [editRowId, setEditRowId] = useState(null); const [editRowData, setEditRowData] = useState>({}); const [newKatDialog, setNewKatDialog] = useState(false); const [newKatForm, setNewKatForm] = useState({ bezeichnung: '', farbe: '#1976d2' }); const createKatMutation = useMutation({ mutationFn: (data: Omit) => kategorieApi.create(data), onSuccess: () => { notification.showSuccess('Kategorie erstellt'); queryClient.invalidateQueries({ queryKey: ['buchungskategorien-all'] }); queryClient.invalidateQueries({ queryKey: ['buchungskategorien'] }); setNewKatDialog(false); setNewKatForm({ bezeichnung: '', farbe: '#1976d2' }); }, onError: () => notification.showError('Fehler beim Erstellen'), }); const updateKatMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial }) => kategorieApi.update(id, data), onSuccess: () => { notification.showSuccess('Kategorie aktualisiert'); queryClient.invalidateQueries({ queryKey: ['buchungskategorien-all'] }); queryClient.invalidateQueries({ queryKey: ['buchungskategorien'] }); setEditRowId(null); }, onError: () => notification.showError('Fehler beim Aktualisieren'), }); const deleteKatMutation = useMutation({ mutationFn: (id: number) => kategorieApi.delete(id), onSuccess: () => { notification.showSuccess('Kategorie deaktiviert'); queryClient.invalidateQueries({ queryKey: ['buchungskategorien-all'] }); queryClient.invalidateQueries({ queryKey: ['buchungskategorien'] }); }, onError: () => notification.showError('Fehler beim Löschen'), }); // ── Render ───────────────────────────────────────────────────────────────── if (!isFeatureEnabled('fahrzeugbuchungen')) { return ; } return ( {/* Header */} Fahrzeugbuchungen {/* Tabs */} setTabIndex(v)}> {canManage && } {/* ── Tab 0: Buchungen ─────────────────────────────────────────────── */} {tabIndex === 0 && ( <> {/* Filters */} setFilterFrom(iso)} sx={{ minWidth: 160 }} /> setFilterTo(iso)} sx={{ minWidth: 160 }} /> Fahrzeug Kategorie {/* Loading */} {isLoading && ( )} {/* Error */} {isError && ( {loadError instanceof Error ? loadError.message : 'Fehler beim Laden'} )} {/* Bookings list */} {!isLoading && !isError && bookings.length === 0 && ( Keine Buchungen vorhanden )} {!isLoading && !isError && bookings.map((booking) => ( {booking.titel} {booking.fahrzeug_name} {booking.fahrzeug_kennzeichen ? ` (${booking.fahrzeug_kennzeichen})` : ''} {formatBookingDate(booking.beginn, booking.ende, booking.ganztaegig)} {booking.gebucht_von_name && ( von {booking.gebucht_von_name} )} {(canManage || booking.gebucht_von === user?.id) && ( navigate(`/fahrzeugbuchungen/${booking.id}`)} > )} {(canManage || booking.gebucht_von === user?.id) && ( { setCancelId(booking.id); setCancelGrund(''); }} > )} ))} )} {/* ── Tab 1: Einstellungen ─────────────────────────────────────────── */} {tabIndex === 1 && canManage && ( Buchungskategorien Bezeichnung Farbe Sortierung Aktiv Aktionen {kategorien.map((kat) => { const isEditing = editRowId === kat.id; return ( {isEditing ? ( setEditRowData((d) => ({ ...d, bezeichnung: e.target.value })) } /> ) : ( kat.bezeichnung )} {isEditing ? ( setEditRowData((d) => ({ ...d, farbe: e.target.value })) } style={{ width: 40, height: 28, border: 'none', cursor: 'pointer' }} /> ) : ( )} {isEditing ? ( setEditRowData((d) => ({ ...d, sort_order: parseInt(e.target.value) || 0, })) } sx={{ width: 80 }} /> ) : ( kat.sort_order )} {isEditing ? ( setEditRowData((d) => ({ ...d, aktiv: e.target.checked })) } /> ) : ( )} {isEditing ? ( updateKatMutation.mutate({ id: kat.id, data: editRowData }) } > { setEditRowId(null); setEditRowData({}); }} > ) : ( { setEditRowId(kat.id); setEditRowData({ bezeichnung: kat.bezeichnung, farbe: kat.farbe, sort_order: kat.sort_order, aktiv: kat.aktiv, }); }} > deleteKatMutation.mutate(kat.id)} > )} ); })} {kategorien.length === 0 && ( Keine Kategorien vorhanden )}
)} {/* ── FAB ── */} {canCreate && tabIndex === 0 && ( navigate('/fahrzeugbuchungen/neu')} > )} {/* ── Cancel dialog ── */} setCancelId(null)} maxWidth="xs" fullWidth > Buchung stornieren setCancelGrund(e.target.value)} sx={{ mt: 1 }} helperText={`${cancelGrund.length}/1000 (min. 5 Zeichen)`} inputProps={{ maxLength: 1000 }} /> {/* ── iCal dialog ── */} setIcalOpen(false)} maxWidth="sm" fullWidth> Kalender abonnieren Abonniere den Fahrzeugbuchungskalender in deiner Kalenderanwendung. Kopiere die URL und füge sie als neuen Kalender (per URL) in Apple Kalender, Google Kalender oder Outlook ein. { navigator.clipboard.writeText(icalUrl); notification.showSuccess('URL kopiert!'); }} > ), }} /> {/* ── New category dialog ── */} setNewKatDialog(false)} maxWidth="xs" fullWidth > Neue Kategorie setNewKatForm((f) => ({ ...f, bezeichnung: e.target.value })) } /> Farbe setNewKatForm((f) => ({ ...f, farbe: e.target.value }))} style={{ width: 60, height: 36, border: 'none', cursor: 'pointer' }} />
); } export default FahrzeugBuchungen;