'use client'; /** * Formular fuer neue Reservierungen (plan.md Abschnitt 15): * Startdatum, Startzeit, Enddatum, Endzeit (15-Minuten-Schritte), * Titel, Notiz. * * Der Server erwartet Start- UND Endzeit (createReservationSchema). * Die maximale Reservierungsdauer wird live gegen die Kalender-Einstellung * geprueft und der Benutzer bei Ueberschreitung informiert. * Der Server prueft Kollisionen; 409 wird verstaendlich angezeigt. */ import { useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { Button, Card, ErrorMessage, Input, Label } from '@/components/ui/ui'; /** Stunden 00-23 (deutsches 24h-Format). */ const HOURS: readonly number[] = Array.from({ length: 24 }, (_, i) => i); /** Minuten in 15-Minuten-Schritten. */ const MINUTES = [0, 15, 30, 45] as const; /** Formatiert Minuten als lesbare Angabe (z. B. "4 Stunden"). */ function formatDuration(minutes: number): string { if (minutes < 60) { return `${minutes} Minuten`; } const hours = minutes / 60; return Number.isInteger(hours) ? `${hours} ${hours === 1 ? 'Stunde' : 'Stunden'}` : `${hours.toFixed(1).replace('.', ',')} Stunden`; } /** Zwei-Dropdown-Zeitwaehler (Stunde + Minute), 24h-Format. */ function TimeSelect({ id, name, value, onChange, }: { id: string; name: string; value: string; onChange: (next: string) => void; }) { const [hourPart, minutePart] = value.split(':'); const hour = hourPart ?? '09'; const minute = minutePart ?? '00'; const selectClasses = 'rounded-md border border-slate-300 bg-white px-2 py-2 text-sm text-slate-900 focus:border-slate-500 focus:outline-none focus:ring-1 focus:ring-slate-500 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100'; return (
); } export function NewReservationButton({ calendarId, maxReservationMinutes = 0, }: { calendarId: string; /** Maximale Reservierungsdauer in Minuten; 0 = unbegrenzt. */ maxReservationMinutes?: number; }) { const router = useRouter(); const [open, setOpen] = useState(false); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [startTime, setStartTime] = useState('09:00'); const [endTime, setEndTime] = useState('10:00'); const today = new Date().toISOString().slice(0, 10); const [startDate, setStartDate] = useState(today); const [endDate, setEndDate] = useState(today); /** Live-Berechnung der Dauer aus Start- und Enddatum/-zeit. */ const durationMinutes = useMemo(() => { const start = new Date(`${startDate}T${startTime}:00`); const end = new Date(`${endDate}T${endTime}:00`); if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) { return null; } const diff = (end.getTime() - start.getTime()) / 60_000; return diff > 0 ? diff : null; }, [startDate, startTime, endDate, endTime]); const exceedsMax = durationMinutes !== null && maxReservationMinutes > 0 && durationMinutes > maxReservationMinutes; const isInvalidRange = durationMinutes === null; async function handleSubmit(event: React.FormEvent) { event.preventDefault(); setBusy(true); setError(null); const formData = new FormData(event.currentTarget); const startAt = new Date(`${startDate}T${startTime}:00`); const endAt = new Date(`${endDate}T${endTime}:00`); if (endAt <= startAt) { setError('Enddatum/-zeit muss nach dem Start liegen.'); setBusy(false); return; } try { const response = await fetch(`/api/calendars/${calendarId}/reservations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ calendarId, startAt: startAt.toISOString(), endAt: endAt.toISOString(), title: formData.get('title'), notes: formData.get('notes') || undefined, }), }); if (!response.ok) { const data = (await response.json()) as { error?: string }; setError(data.error ?? 'Reservierung fehlgeschlagen.'); return; } setOpen(false); router.refresh(); } catch { setError('Netzwerkfehler. Bitte erneut versuchen.'); } finally { setBusy(false); } } if (!open) { return ( ); } return (

Neue Reservierung

setStartDate(e.target.value)} />
setEndDate(e.target.value)} />
{durationMinutes !== null && (

Dauer: {formatDuration(durationMinutes)} {exceedsMax && ` — Maximale Reservierungsdauer ist ${formatDuration(maxReservationMinutes)}.`}

)} {isInvalidRange && (

Enddatum/-zeit muss nach dem Start liegen.

)}
); }