import { useRef, useState } from 'react' import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-router' import { StaffTabs } from '#/components/staff-tabs' import { getCurrentStaffUser } from '#/lib/auth' import { authClient } from '#/lib/auth-client' import { getTables, setTableMarker } from '#/lib/reservations' import { getReservationSettings, updateReservationSettings } from '#/lib/settings' import { ACCEPTED_IMAGE_TYPES, MAX_TABLE_IMAGE_BYTES, readImageAsDataUrl } from '#/lib/table-image' export const Route = createFileRoute('/staff/settings')({ component: StaffSettings, beforeLoad: async () => { const staffUser = await getCurrentStaffUser() if (!staffUser) { throw redirect({ to: '/staff/login' }) } if (staffUser.role !== 'admin') { throw redirect({ to: '/staff' }) } return { staffUser } }, loader: async ({ context }) => { const [settings, tables] = await Promise.all([getReservationSettings(), getTables()]) return { staffUser: context.staffUser, settings, tables } }, }) type FormState = { reservationIntervalMinutes: number reservationDurationMinutes: number minLeadTimeMinutes: number maxLeadTimeDays: number tableDisplayMode: 'individual' | 'overview' tableOverviewImageUrl: string | null } const MAX_IMAGE_MB = Math.floor(MAX_TABLE_IMAGE_BYTES / 1_000_000) function StaffSettings() { const router = useRouter() const { staffUser, settings, tables } = Route.useLoaderData() const [form, setForm] = useState({ reservationIntervalMinutes: settings.reservationIntervalMinutes, reservationDurationMinutes: settings.reservationDurationMinutes, minLeadTimeMinutes: settings.minLeadTimeMinutes, maxLeadTimeDays: settings.maxLeadTimeDays, tableDisplayMode: settings.tableDisplayMode === 'overview' ? 'overview' : 'individual', tableOverviewImageUrl: settings.tableOverviewImageUrl, }) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) const [isSubmitting, setIsSubmitting] = useState(false) const overviewFileInputRef = useRef(null) const [markerTableId, setMarkerTableId] = useState('') const [isPlacingMarker, setIsPlacingMarker] = useState(false) const [markerError, setMarkerError] = useState(null) const [markerSuccess, setMarkerSuccess] = useState(null) const markerTable = tables.find((table) => String(table.id) === markerTableId) const tablesWithoutMarker = tables.filter((table) => table.markerX === null || table.markerY === null) async function handleLogout() { await authClient.signOut() await router.navigate({ to: '/staff/login' }) await router.invalidate() } async function handleOverviewImageChange(event: React.ChangeEvent) { const file = event.target.files?.[0] event.target.value = '' if (!file) return if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { setError('Bild muss PNG, JPEG oder WebP sein') return } if (file.size > MAX_TABLE_IMAGE_BYTES) { setError(`Bild ist zu groß (max. ${MAX_IMAGE_MB} MB)`) return } setError(null) const dataUrl = await readImageAsDataUrl(file) setForm((f) => ({ ...f, tableOverviewImageUrl: dataUrl })) } async function handlePlaceMarker(event: React.MouseEvent) { if (!markerTable) { setMarkerError('Bitte zuerst einen Tisch für den Marker auswählen') return } const rect = event.currentTarget.getBoundingClientRect() const x = Math.min(100, Math.max(0, ((event.clientX - rect.left) / rect.width) * 100)) const y = Math.min(100, Math.max(0, ((event.clientY - rect.top) / rect.height) * 100)) setMarkerError(null) setMarkerSuccess(null) setIsPlacingMarker(true) try { await setTableMarker({ data: { id: markerTable.id, marker: { x, y } } }) setMarkerSuccess(`Marker für ${markerTable.name} wurde gesetzt.`) await router.invalidate() } catch (err) { setMarkerError(err instanceof Error ? err.message : 'Etwas ist schiefgelaufen') } finally { setIsPlacingMarker(false) } } async function handleClearMarker() { if (!markerTable) return setMarkerError(null) setMarkerSuccess(null) setIsPlacingMarker(true) try { await setTableMarker({ data: { id: markerTable.id, marker: null } }) setMarkerSuccess(`Marker für ${markerTable.name} wurde entfernt.`) await router.invalidate() } catch (err) { setMarkerError(err instanceof Error ? err.message : 'Etwas ist schiefgelaufen') } finally { setIsPlacingMarker(false) } } async function handleSubmit(event: React.FormEvent) { event.preventDefault() setError(null) setSuccess(null) setIsSubmitting(true) try { await updateReservationSettings({ data: form }) setSuccess('Einstellungen wurden gespeichert.') await router.invalidate() } catch (err) { setError(err instanceof Error ? err.message : 'Etwas ist schiefgelaufen') } finally { setIsSubmitting(false) } } return (
Steakhaus bei Mirko
Angemeldet als {staffUser.name}

Einstellungen

Reservierungsregeln

Abstand zwischen den wählbaren Uhrzeiten im Buchungsformular.

Wie lange ein Tisch nach einer Reservierung als belegt gilt — verhindert überlappende Buchungen.

Wie kurzfristig frühestens reserviert werden kann, z. B. 60 für „mindestens eine Stunde im Voraus“.

Wie weit im Voraus reserviert werden kann, z. B. 60 für „höchstens 60 Tage im Voraus“.

Tischfotos im Buchungsformular

„Foto des gewählten Tisches“ zeigt das jeweilige Foto aus der Tischverwaltung, sobald ein Tisch ausgewählt wird. „Übersichtsfoto“ zeigt stattdessen immer dasselbe Bild, z. B. eine Aufnahme des ganzen Raums.

{form.tableDisplayMode === 'overview' && ( <>

PNG, JPEG oder WebP, max. {MAX_IMAGE_MB} MB.

)} {error &&

{error}

} {success &&

{success}

}
{settings.tableDisplayMode === 'overview' && settings.tableOverviewImageUrl && (

Tisch-Marker

Wählen Sie einen Tisch aus und klicken Sie dann auf das gespeicherte Übersichtsfoto, um seinen Marker zu setzen oder zu verschieben.

{tables .filter((table) => table.markerX !== null && table.markerY !== null) .map((table) => ( {table.tableNumber ?? '?'} ))}
{markerTable && (markerTable.markerX !== null || markerTable.markerY !== null) && ( )} {tablesWithoutMarker.length > 0 && (

Noch ohne Marker: {tablesWithoutMarker.map((table) => table.name).join(', ')}

)} {markerError &&

{markerError}

} {markerSuccess &&

{markerSuccess}

}
)}
) }