feat(frontend): add full reservation workflow, timeline gantt, and booking modal

This commit is contained in:
DanielS
2026-08-19 00:32:33 +02:00
parent 737686692a
commit 769268a77e
12 changed files with 1897 additions and 327 deletions

View File

@@ -0,0 +1,118 @@
import { RestaurantTable, TableStatus } from '../types';
import { useAppStore } from '../store/useAppStore';
const STATUS_STYLES: Record<TableStatus, string> = {
FREE: 'border-emerald-500 bg-emerald-500/15 text-emerald-300',
OCCUPIED: 'border-rose-500 bg-rose-500/15 text-rose-300',
RESERVED: 'border-amber-400 bg-amber-400/15 text-amber-300',
BILL: 'border-sky-400 bg-sky-400/15 text-sky-300',
};
const STATUS_LABEL: Record<TableStatus, string> = {
FREE: 'Frei', OCCUPIED: 'Belegt', RESERVED: 'Reserviert', BILL: 'Rechnung',
};
interface Props {
tables: RestaurantTable[];
}
export default function FloorPlan({ tables }: Props) {
const { selectedTableId, selectTable, setTableStatus, openBookingModal } = useAppStore();
const handleTableClick = (t: RestaurantTable) => {
selectTable(t.id === selectedTableId ? null : t.id);
};
const selected = tables.find((t) => t.id === selectedTableId);
return (
<div className="flex gap-4 flex-col lg:flex-row h-full">
{/* Canvas */}
<div className="flex-1 relative bg-slate-950/70 rounded-2xl border border-slate-800 overflow-hidden min-h-[420px]">
{/* Grid */}
<div
className="absolute inset-0 opacity-20"
style={{
backgroundImage: 'linear-gradient(to right, #334155 1px, transparent 1px), linear-gradient(to bottom, #334155 1px, transparent 1px)',
backgroundSize: '40px 40px',
}}
/>
{/* Area labels */}
{['Gastraum', 'Terrasse', 'Bar'].map((area, i) => (
<div
key={area}
className="absolute text-xs font-semibold text-slate-600 uppercase tracking-widest select-none"
style={{ left: 16, top: 16 + i * 200 }}
>
{area}
</div>
))}
{tables.map((t) => (
<button
key={t.id}
onClick={() => handleTableClick(t)}
style={{ left: t.x, top: t.y }}
title={`${t.number}${STATUS_LABEL[t.status]}`}
className={`absolute w-28 h-28 border-2 transition-all duration-200 flex flex-col items-center justify-center gap-1 group
${t.shape === 'ROUND' ? 'rounded-full' : 'rounded-2xl'}
${STATUS_STYLES[t.status]}
${selectedTableId === t.id ? 'ring-4 ring-amber-400/50 scale-110 z-10' : 'hover:scale-105'}
cursor-pointer shadow-lg`}
>
<span className="font-bold text-xl">{t.number}</span>
<span className="text-xs opacity-75">👤 {t.maxCapacity}</span>
<span className="text-[10px] font-semibold uppercase tracking-wider opacity-80">
{STATUS_LABEL[t.status]}
</span>
</button>
))}
</div>
{/* Side Panel */}
<div className="w-full lg:w-64 flex flex-col gap-3">
{/* Legend */}
<div className="bg-slate-900/60 border border-slate-800 rounded-2xl p-4 space-y-2">
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">Legende</p>
{(Object.entries(STATUS_LABEL) as [TableStatus, string][]).map(([s, label]) => (
<div key={s} className="flex items-center gap-2 text-sm">
<span className={`w-3 h-3 rounded border ${STATUS_STYLES[s]}`} />
<span className="text-slate-300">{label}</span>
</div>
))}
</div>
{/* Selected Table Control */}
{selected && (
<div className="bg-slate-900/60 border border-amber-500/30 rounded-2xl p-4 space-y-3">
<p className="text-sm font-bold text-amber-400">Tisch {selected.number}</p>
<p className="text-xs text-slate-400">{selected.area} · max. {selected.maxCapacity} Pers.</p>
<div className="grid grid-cols-2 gap-2">
{(['FREE', 'OCCUPIED', 'RESERVED', 'BILL'] as TableStatus[]).map((s) => (
<button
key={s}
onClick={() => setTableStatus(selected.id, s)}
className={`px-2 py-1.5 rounded-lg text-xs font-semibold border transition
${selected.status === s
? STATUS_STYLES[s] + ' shadow-md'
: 'border-slate-700 text-slate-500 hover:border-slate-500'}`}
>
{STATUS_LABEL[s]}
</button>
))}
</div>
<button
onClick={() => openBookingModal(selected.id)}
className="w-full py-2 rounded-xl bg-amber-500 hover:bg-amber-400 text-slate-950 font-semibold text-xs transition active:scale-95"
>
+ Neue Reservierung
</button>
</div>
)}
</div>
</div>
);
}