'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
);
}