import { useState } from 'react' import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-router' import { ConfirmDialog } from '#/components/confirm-dialog' import { ReservationCharts } from '#/components/reservation-charts' import { ReservationDateTimeField } from '#/components/reservation-datetime-field' import { StaffTabs } from '#/components/staff-tabs' import { getCurrentStaffUser } from '#/lib/auth' import { authClient } from '#/lib/auth-client' import { formatDateOnly, formatDateTimeLocal } from '#/lib/datetime' import { generateTimeOptions, getNextAvailableSlot, getOpeningRangesForWeekday } from '#/lib/opening-hours' import { createReservation, deleteReservation, getReservations, getTables, updateReservation, } from '#/lib/reservations' import { getReservationSettings } from '#/lib/settings' export const Route = createFileRoute('/staff/')({ component: StaffDashboard, beforeLoad: async () => { const staffUser = await getCurrentStaffUser() if (!staffUser) { throw redirect({ to: '/staff/login' }) } return { staffUser } }, loader: async ({ context }) => { const [reservations, tables, settings] = await Promise.all([ getReservations(), getTables(), getReservationSettings(), ]) return { staffUser: context.staffUser, reservations, tables, settings } }, }) const dateTimeFormatter = new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium', timeStyle: 'short', }) function isSameDay(a: Date, b: Date) { return ( a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() ) } type ReservationRow = Awaited>[number] type FormState = { tableId: number | string customerName: string customerEmail: string partySize: number reservationTime: string } type SettingsRow = Awaited> function emptyForm(defaultTableId: number | string, settings: SettingsRow): FormState { const slot = getNextAvailableSlot( new Date(Date.now() + settings.minLeadTimeMinutes * 60_000), settings.reservationIntervalMinutes, ) return { tableId: defaultTableId, customerName: '', customerEmail: '', partySize: 2, reservationTime: slot ? `${slot.date}T${slot.time}` : '', } } function StaffDashboard() { const router = useRouter() const { staffUser, reservations, tables, settings } = Route.useLoaderData() const [editingId, setEditingId] = useState(null) const [form, setForm] = useState(() => emptyForm(tables[0]?.id ?? '', settings)) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) const [isSubmitting, setIsSubmitting] = useState(false) const [deletingId, setDeletingId] = useState(null) const [pendingDelete, setPendingDelete] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [tableFilter, setTableFilter] = useState('all') const [timeFilter, setTimeFilter] = useState<'all' | 'today' | 'upcoming' | 'past'>('all') const now = new Date() const todaysReservations = reservations.filter((r) => isSameDay(new Date(r.reservationTime), now)) const guestsToday = todaysReservations.reduce((sum, r) => sum + r.partySize, 0) const upcoming = reservations.filter((r) => new Date(r.reservationTime).getTime() >= now.getTime()) const filteredReservations = reservations.filter((r) => { if (tableFilter !== 'all' && String(r.tableId) !== tableFilter) return false const reservationTime = new Date(r.reservationTime) if (timeFilter === 'today' && !isSameDay(reservationTime, now)) return false if (timeFilter === 'upcoming' && reservationTime.getTime() < now.getTime()) return false if (timeFilter === 'past' && reservationTime.getTime() >= now.getTime()) return false const query = searchQuery.trim().toLowerCase() if (query) { const haystack = `${r.customerName} ${r.customerEmail} ${r.tableName}`.toLowerCase() if (!haystack.includes(query)) return false } return true }) // Other reservations already sitting on the currently selected table + date, so the // time picker can block them instead of letting staff pick a slot the server will // just reject as double-booked. A reservation occupies its whole configured // duration, not just its exact start time, so every interval-grid slot that falls // inside that window counts as taken too. const [selectedDate] = form.reservationTime.split('T') const otherReservationsOnSelection = reservations.filter( (r) => r.id !== editingId && String(r.tableId) === String(form.tableId) && formatDateOnly(new Date(r.reservationTime)) === selectedDate, ) const bookedTimesForForm = selectedDate ? generateTimeOptions( getOpeningRangesForWeekday(new Date(`${selectedDate}T00:00`).getDay()), settings.reservationIntervalMinutes, ).filter((option) => { const slotStart = new Date(`${selectedDate}T${option}`).getTime() return otherReservationsOnSelection.some((r) => { const otherStart = new Date(r.reservationTime).getTime() const otherEnd = otherStart + r.durationMinutes * 60_000 return slotStart >= otherStart && slotStart < otherEnd }) }) : [] function startEdit(reservation: ReservationRow) { setEditingId(reservation.id) setForm({ tableId: reservation.tableId, customerName: reservation.customerName, customerEmail: reservation.customerEmail, partySize: reservation.partySize, reservationTime: formatDateTimeLocal(new Date(reservation.reservationTime)), }) setError(null) setSuccess(null) } function cancelEdit() { setEditingId(null) setForm(emptyForm(tables[0]?.id ?? '', settings)) setError(null) } async function handleSubmit(event: React.FormEvent) { event.preventDefault() setError(null) setSuccess(null) if (!form.tableId) { setError('Bitte wählen Sie einen Tisch aus') return } if (!form.reservationTime.split('T')[1]) { setError('Bitte wählen Sie eine gültige Uhrzeit aus') return } setIsSubmitting(true) try { const payload = { tableId: Number(form.tableId), customerName: form.customerName, customerEmail: form.customerEmail, partySize: Number(form.partySize), reservationTime: new Date(form.reservationTime).toISOString(), } if (editingId) { await updateReservation({ data: { id: editingId, ...payload } }) setSuccess('Reservierung wurde aktualisiert.') } else { await createReservation({ data: payload }) setSuccess('Reservierung wurde angelegt.') } setEditingId(null) setForm(emptyForm(tables[0]?.id ?? '', settings)) await router.invalidate() } catch (err) { setError(err instanceof Error ? err.message : 'Etwas ist schiefgelaufen') } finally { setIsSubmitting(false) } } async function confirmDelete() { const reservation = pendingDelete if (!reservation) return setPendingDelete(null) setDeletingId(reservation.id) setError(null) setSuccess(null) try { await deleteReservation({ data: { id: reservation.id } }) if (editingId === reservation.id) { cancelEdit() } setSuccess('Reservierung wurde gelöscht.') await router.invalidate() } catch (err) { setError(err instanceof Error ? err.message : 'Etwas ist schiefgelaufen') } finally { setDeletingId(null) } } async function handleLogout() { await authClient.signOut() await router.navigate({ to: '/staff/login' }) await router.invalidate() } return (
Steakhaus bei Mirko
Angemeldet als {staffUser.name}

Reservierungen

{reservations.length} Reservierungen gesamt
{todaysReservations.length} Heute
{guestsToday} Gäste heute
{upcoming.length} Anstehend

{editingId ? 'Reservierung bearbeiten' : 'Neue Reservierung'}

{editingId && ( )}
{error &&

{error}

} {success &&

{success}

}
{reservations.length > 0 && (
setSearchQuery(e.target.value)} aria-label="Reservierungen durchsuchen" />
)} {filteredReservations.length === 0 ? (

{reservations.length === 0 ? 'Noch keine Reservierungen vorhanden.' : 'Keine Reservierungen für diese Filter gefunden.'}

) : (
{filteredReservations.map((reservation) => ( ))}
Datum & Uhrzeit Tisch Kunde Personen
{dateTimeFormatter.format(new Date(reservation.reservationTime))} {reservation.tableName} {reservation.customerName}
{reservation.customerEmail}
{reservation.partySize}
)}
setPendingDelete(null)} />
) }