279 lines
12 KiB
TypeScript
279 lines
12 KiB
TypeScript
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];
|
|
|
|
function generateId() {
|
|
return Math.random().toString(36).substring(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;
|
|
}
|
|
|
|
const TIME_SLOTS = getTimeSlots();
|
|
|
|
export default function BookingModal() {
|
|
const { tables, prefillTableId, prefillTime, prefillDate, closeBookingModal, addReservation } = useAppStore();
|
|
|
|
const today = prefillDate ?? new Date().toISOString().slice(0, 10);
|
|
|
|
const [guestCount, setGuestCount] = useState(2);
|
|
const [date, setDate] = useState(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 [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<string[]>([]);
|
|
|
|
const suggestedTables = tables.filter((t) => t.maxCapacity >= guestCount && t.status === 'FREE');
|
|
|
|
const toggleTag = (tag: string) =>
|
|
setSelectedTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]);
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!firstName || !tableId) return;
|
|
|
|
const reservation: Reservation = {
|
|
id: generateId(),
|
|
guest: { id: generateId(), firstName, lastName, phone: phone || undefined, email: email || undefined },
|
|
tableIds: [tableId],
|
|
date,
|
|
startTime,
|
|
endTime,
|
|
guestCount,
|
|
status: 'CONFIRMED',
|
|
notes,
|
|
tags: selectedTags,
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
addReservation(reservation);
|
|
closeBookingModal();
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-slate-950/85 backdrop-blur-sm flex items-center justify-center p-4 z-50 overflow-y-auto">
|
|
<div className="bg-slate-900 border border-slate-700 rounded-3xl w-full max-w-lg shadow-2xl my-4">
|
|
{/* Header */}
|
|
<div className="px-6 py-5 border-b border-slate-800 flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-amber-400">Neue Reservierung</h3>
|
|
<p className="text-xs text-slate-400">Schnellbuchung / Walk-In</p>
|
|
</div>
|
|
<button
|
|
onClick={closeBookingModal}
|
|
className="w-8 h-8 rounded-full bg-slate-800 hover:bg-slate-700 flex items-center justify-center text-slate-400 hover:text-white transition"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-6 space-y-5">
|
|
{/* Person Count */}
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Personen</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{GUEST_COUNTS.map((n) => (
|
|
<button
|
|
key={n}
|
|
type="button"
|
|
onClick={() => setGuestCount(n)}
|
|
className={`w-11 h-11 rounded-xl font-bold text-sm transition border ${
|
|
guestCount === n
|
|
? 'bg-amber-500 border-amber-400 text-slate-950 shadow-lg shadow-amber-500/20'
|
|
: 'bg-slate-800/60 border-slate-700 text-slate-300 hover:border-amber-500/50'
|
|
}`}
|
|
>
|
|
{n}
|
|
</button>
|
|
))}
|
|
<button
|
|
type="button"
|
|
onClick={() => setGuestCount(15)}
|
|
className={`px-3 h-11 rounded-xl font-bold text-sm transition border ${
|
|
guestCount >= 13
|
|
? 'bg-amber-500 border-amber-400 text-slate-950'
|
|
: 'bg-slate-800/60 border-slate-700 text-slate-300 hover:border-amber-500/50'
|
|
}`}
|
|
>
|
|
12+
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Date & Time */}
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<div className="col-span-3 sm:col-span-1">
|
|
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Datum</label>
|
|
<input
|
|
type="date"
|
|
value={date}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Von</label>
|
|
<select
|
|
value={startTime}
|
|
onChange={(e) => setStartTime(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"
|
|
>
|
|
{TIME_SLOTS.map((s) => <option key={s}>{s}</option>)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Bis</label>
|
|
<select
|
|
value={endTime}
|
|
onChange={(e) => setEndTime(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"
|
|
>
|
|
{TIME_SLOTS.map((s) => <option key={s}>{s}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Guest Info */}
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Gastdaten</label>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<input
|
|
required
|
|
type="text"
|
|
placeholder="Vorname *"
|
|
value={firstName}
|
|
onChange={(e) => 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"
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Nachname"
|
|
value={lastName}
|
|
onChange={(e) => 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"
|
|
/>
|
|
<input
|
|
type="tel"
|
|
placeholder="Telefon"
|
|
value={phone}
|
|
onChange={(e) => 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"
|
|
/>
|
|
<input
|
|
type="email"
|
|
placeholder="E-Mail (optional)"
|
|
value={email}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table Selection */}
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">
|
|
Tisch zuweisen
|
|
{suggestedTables.length > 0 && (
|
|
<span className="ml-2 text-emerald-400 font-medium normal-case">
|
|
{suggestedTables.length} freie Tische verfügbar
|
|
</span>
|
|
)}
|
|
</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{tables.map((t) => {
|
|
const isSuggested = suggestedTables.some((s) => s.id === t.id);
|
|
return (
|
|
<button
|
|
key={t.id}
|
|
type="button"
|
|
onClick={() => setTableId(t.id)}
|
|
className={`px-3 py-2 rounded-xl text-xs font-semibold border transition ${
|
|
tableId === t.id
|
|
? 'bg-amber-500 border-amber-400 text-slate-950'
|
|
: isSuggested
|
|
? 'bg-emerald-500/15 border-emerald-500/40 text-emerald-300 hover:bg-emerald-500/25'
|
|
: 'bg-slate-800/50 border-slate-700 text-slate-400 hover:border-slate-500'
|
|
}`}
|
|
>
|
|
{t.number} ({t.maxCapacity}P)
|
|
{isSuggested && tableId !== t.id && <span className="ml-1">✓</span>}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tags */}
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Tags / Besonderheiten</label>
|
|
<div className="flex gap-2 flex-wrap">
|
|
{TAGS.map((tag) => (
|
|
<button
|
|
key={tag}
|
|
type="button"
|
|
onClick={() => toggleTag(tag)}
|
|
className={`px-3 py-1.5 rounded-full text-xs font-semibold border transition ${
|
|
selectedTags.includes(tag)
|
|
? 'bg-purple-500/30 border-purple-400/60 text-purple-300'
|
|
: 'bg-slate-800/50 border-slate-700 text-slate-400 hover:border-slate-500'
|
|
}`}
|
|
>
|
|
{tag === 'Hochstuhl' ? '🪑' : tag === 'Allergie' ? '⚠️' : tag === 'Fensterplatz' ? '🪟' : '🎂'} {tag}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notes */}
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Notizen</label>
|
|
<textarea
|
|
rows={2}
|
|
placeholder="Besondere Wünsche, Hinweise..."
|
|
value={notes}
|
|
onChange={(e) => setNotes(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 resize-none placeholder-slate-600"
|
|
/>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex gap-3 pt-1">
|
|
<button
|
|
type="button"
|
|
onClick={closeBookingModal}
|
|
className="flex-1 py-3 rounded-2xl bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold transition text-sm"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="flex-1 py-3 rounded-2xl bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold transition text-sm active:scale-95 shadow-lg shadow-amber-500/25"
|
|
>
|
|
Reservierung bestätigen
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|