diff --git a/frontend/src/components/BookingModal.tsx b/frontend/src/components/BookingModal.tsx index 9c5d6eb..304cfbe 100644 --- a/frontend/src/components/BookingModal.tsx +++ b/frontend/src/components/BookingModal.tsx @@ -2,273 +2,361 @@ import { useState } from 'react'; import { useAppStore } from '../store/useAppStore'; import { Reservation } from '../types'; -const TAGS = ['Hochstuhl', 'Allergie', 'Fensterplatz', 'Geburtstag']; -const GUEST_COUNTS = [1, 2, 3, 4, 5, 6, 7, 8, 10, 12]; +// ─── Config ──────────────────────────────────────────────────────────────── +const PERSON_COUNTS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +const TIME_SLOTS = [ + '17:00', '17:30', '18:00', '18:30', '19:00', '19:30', + '20:00', '20:30', '21:00', '21:30', '22:00', '22:30', +]; + +const DURATION_OPTIONS = [ + { label: '1 Std.', value: 60 }, + { label: '1,5 Std.', value: 90 }, + { label: '2 Std.', value: 120 }, + { label: '3 Std.', value: 180 }, +]; + +const EXTRA_TAGS = [ + { id: 'Hochstuhl', icon: '🪑', label: 'Hochstuhl' }, + { id: 'Geburtstag', icon: '🎂', label: 'Geburtstag' }, + { id: 'Fensterplatz', icon: '🪟', label: 'Fensterplatz' }, + { id: 'Hund', icon: '🐕', label: 'Hund' }, + { id: 'Terrasse', icon: '☀️', label: 'Terrasse / Außen' }, + { id: 'Allergie', icon: '⚠️', label: 'Allergie' }, +]; + +// ─── Helpers ──────────────────────────────────────────────────────────────── function generateId() { - return Math.random().toString(36).substring(2, 10); + return Math.random().toString(36).slice(2, 10); } -function getTimeSlots() { - const slots: string[] = []; - for (let h = 10; h < 24; h++) { - for (const m of [0, 15, 30, 45]) { - slots.push(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`); - } - } - return slots; +function addMinutes(time: string, minutes: number): string { + const [h, m] = time.split(':').map(Number); + const total = h * 60 + m + minutes; + return `${String(Math.floor(total / 60) % 24).padStart(2, '0')}:${String(total % 60).padStart(2, '0')}`; } -const TIME_SLOTS = getTimeSlots(); +function today(): string { + return new Date().toISOString().slice(0, 10); +} -export default function BookingModal() { - const { tables, prefillTableId, prefillTime, prefillDate, closeBookingModal, addReservation } = useAppStore(); - - const today = prefillDate ?? new Date().toISOString().slice(0, 10); +// ─── Component ────────────────────────────────────────────────────────────── +export default function ReservationModal() { + const { tables, prefillTableId, prefillTime, prefillDate, closeBookingModal, addReservation, setTableStatus } = useAppStore(); + // ── State ── const [guestCount, setGuestCount] = useState(2); - const [date, setDate] = useState(today); + const [customCount, setCustomCount] = useState(''); + const [date, setDate] = useState(prefillDate ?? today()); const [startTime, setStartTime] = useState(prefillTime ?? '19:00'); - const [endTime, setEndTime] = useState(() => { - const start = prefillTime ?? '19:00'; - const [h, m] = start.split(':').map(Number); - const end = h + 1 + ':' + String(m).padStart(2, '0'); - return end; - }); + const [duration, setDuration] = useState(90); + const [tags, setTags] = useState([]); + const [notes, setNotes] = useState(''); const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [phone, setPhone] = useState(''); const [email, setEmail] = useState(''); - const [tableId, setTableId] = useState(prefillTableId ?? tables[0]?.id ?? ''); - const [notes, setNotes] = useState(''); - const [selectedTags, setSelectedTags] = useState([]); + const [tableId, setTableId] = useState(prefillTableId ?? 'auto'); - const suggestedTables = tables.filter((t) => t.maxCapacity >= guestCount && t.status === 'FREE'); + // ── Derived ── + const effectiveCount = customCount ? parseInt(customCount) : guestCount; + const eligibleTables = tables.filter((t) => t.maxCapacity >= effectiveCount && t.minCapacity <= effectiveCount && t.status === 'FREE'); + const resolvedTableId = tableId === 'auto' ? (eligibleTables[0]?.id ?? tables[0]?.id) : tableId; - const toggleTag = (tag: string) => - setSelectedTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]); + // ── Handlers ── + const toggleTag = (id: string) => + setTags((prev) => prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id]); - const handleSubmit = (e: React.FormEvent) => { + const handleSave = (e: React.FormEvent) => { e.preventDefault(); - if (!firstName || !tableId) return; + if (!firstName.trim()) return; + const endTime = addMinutes(startTime, duration); const reservation: Reservation = { id: generateId(), - guest: { id: generateId(), firstName, lastName, phone: phone || undefined, email: email || undefined }, - tableIds: [tableId], + guest: { + id: generateId(), + firstName: firstName.trim(), + lastName: lastName.trim(), + phone: phone.trim() || undefined, + email: email.trim() || undefined, + }, + tableIds: resolvedTableId ? [resolvedTableId] : [], date, startTime, endTime, - guestCount, + guestCount: effectiveCount, status: 'CONFIRMED', - notes, - tags: selectedTags, + notes: notes.trim(), + tags, createdAt: new Date().toISOString(), }; + addReservation(reservation); + if (resolvedTableId) setTableStatus(resolvedTableId, 'RESERVED'); closeBookingModal(); }; return ( -
-
- {/* Header */} -
+
+
+ + {/* ── Header ── */} +
-

Neue Reservierung

-

Schnellbuchung / Walk-In

+

Neue Reservierung

+

Schnellbuchung · Walk-In

-
- {/* Person Count */} -
- -
- {GUEST_COUNTS.map((n) => ( - - ))} - -
-
+ +
- {/* Date & Time */} -
-
- - setDate(e.target.value)} - className="w-full bg-slate-800/60 border border-slate-700 rounded-xl px-3 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-amber-500 transition" - /> -
-
- - -
-
- - -
-
+ {/* ── Section 1: Eckdaten ── */} +
+ - {/* Guest Info */} -
- -
- setFirstName(e.target.value)} - className="bg-slate-800/60 border border-slate-700 rounded-xl px-3 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-amber-500 transition placeholder-slate-600" - /> - setLastName(e.target.value)} - className="bg-slate-800/60 border border-slate-700 rounded-xl px-3 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-amber-500 transition placeholder-slate-600" - /> - setPhone(e.target.value)} - className="bg-slate-800/60 border border-slate-700 rounded-xl px-3 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-amber-500 transition placeholder-slate-600" - /> - setEmail(e.target.value)} - className="bg-slate-800/60 border border-slate-700 rounded-xl px-3 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-amber-500 transition placeholder-slate-600" - /> -
-
+ {/* Person Count */} +
+ Personenanzahl +
+ {PERSON_COUNTS.map((n) => ( + { setGuestCount(n); setCustomCount(''); }} + className="w-12 h-12 text-base" + > + {n} + + ))} + {/* 10+ */} +
+ { setCustomCount(e.target.value); setGuestCount(0); }} + className="w-full h-full text-center text-sm font-bold bg-transparent text-slate-100 focus:outline-none placeholder-slate-600" + /> +
+
+ {eligibleTables.length > 0 && ( +

+ ✓ {eligibleTables.length} freier Tisch{eligibleTables.length !== 1 ? 'e' : ''} verfügbar +

+ )} + {eligibleTables.length === 0 && effectiveCount > 0 && ( +

+ ⚠ Kein freier Tisch für {effectiveCount} Personen +

+ )} +
- {/* Table Selection */} -
- -
- {tables.map((t) => { - const isSuggested = suggestedTables.some((s) => s.id === t.id); - return ( + {/* Date */} +
+ Datum + setDate(e.target.value)} + className="mt-2 bg-slate-800/60 border border-slate-700 rounded-2xl px-4 py-3 text-sm text-slate-100 focus:outline-none focus:border-amber-500 transition w-full sm:w-56" + /> +
+ + {/* Time Slots */} +
+ Uhrzeit +
+ {TIME_SLOTS.map((t) => ( + setStartTime(t)} + className="px-4 h-11 text-sm font-mono" + > + {t} + + ))} +
+
+ + {/* Duration */} +
+ Aufenthaltsdauer +
+ {DURATION_OPTIONS.map((d) => ( + setDuration(d.value)} + className="px-4 h-11 text-sm" + > + {d.label} + + ))} +
+

+ Ende: {addMinutes(startTime, duration)} +

+
+
+ + + + {/* ── Section 2: Extras & Tags ── */} +
+ + +
+ {EXTRA_TAGS.map((tag) => ( - ); - })} -
-
+ ))} +
- {/* Tags */} -
- -
- {TAGS.map((tag) => ( +
+ Sonstige Wünsche / Notizen +