feat(api): add REST controllers, services, entities and 15-min timeline grid

This commit is contained in:
DanielS
2026-08-19 00:44:33 +02:00
parent 52b0728510
commit 22469f557c
16 changed files with 619 additions and 17 deletions

View File

@@ -0,0 +1,20 @@
import axios from 'axios';
import { Reservation } from '../types';
const BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:8080/api/v1';
const http = axios.create({ baseURL: BASE });
export const api = {
getTables: () =>
http.get('/tables').then((r) => r.data),
getReservations: (date?: string) =>
http.get('/reservations', { params: { date } }).then((r) => r.data),
createReservation: (payload: Partial<Reservation>) =>
http.post('/reservations', payload).then((r) => r.data),
updateStatus: (id: string, status: string) =>
http.patch(`/reservations/${id}/status`, { status }).then((r) => r.data),
};

View File

@@ -1,7 +1,14 @@
import { useAppStore } from '../store/useAppStore';
import { Reservation, ReservationStatus } from '../types';
const HOURS = Array.from({ length: 14 }, (_, i) => `${String(i + 10).padStart(2, '0')}:00`);
// 15-min slots from 10:00 to 24:00
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')}`);
}
}
const HOUR_SLOTS = SLOTS.filter((s) => s.endsWith(':00'));
const STATUS_DOT: Record<ReservationStatus, string> = {
PENDING: 'bg-slate-400',
@@ -29,7 +36,8 @@ function timeToMinutes(t: string) {
const TIMELINE_START = 10 * 60; // 10:00
const TIMELINE_END = 24 * 60; // 24:00
const TOTAL_MINUTES = TIMELINE_END - TIMELINE_START;
const COL_WIDTH = 72; // px per hour
const SLOT_WIDTH = 18; // px per 15-min slot
const HOUR_WIDTH = SLOT_WIDTH * 4; // 72px per hour
export default function Timeline() {
const { tables, reservations, openBookingModal } = useAppStore();
@@ -46,11 +54,12 @@ export default function Timeline() {
<div className="w-[120px] shrink-0 p-3 text-xs font-semibold text-slate-500 uppercase tracking-wider border-r border-slate-800">
Tisch
</div>
{HOURS.map((h) => (
{/* Hour labels */}
{HOUR_SLOTS.map((h) => (
<div
key={h}
className="text-center text-xs text-slate-500 border-r border-slate-800/50 py-3 font-mono"
style={{ width: COL_WIDTH }}
style={{ width: HOUR_WIDTH }}
>
{h}
</div>
@@ -72,15 +81,16 @@ export default function Timeline() {
</div>
{/* Timeline cells */}
<div className="relative flex flex-1" style={{ height: 64 }}>
{/* Click grid */}
{HOURS.map((_, i) => (
<div className="relative flex flex-1" style={{ height: 56 }}>
{/* 15-min clickable slots */}
{SLOTS.map((slot, i) => (
<button
key={i}
onClick={() => handleSlotClick(table.id, 10 + i)}
className="border-r border-slate-800/30 h-full hover:bg-amber-500/10 transition shrink-0 group-hover:border-slate-700/50"
style={{ width: COL_WIDTH }}
title={`Neue Reservierung: ${table.number} um ${HOURS[i]}`}
onClick={() => handleSlotClick(table.id, parseInt(slot.split(':')[0]))}
className={`border-r h-full hover:bg-indigo-500/10 transition shrink-0
${slot.endsWith(':00') ? 'border-slate-700/60' : 'border-slate-800/30'}`}
style={{ width: SLOT_WIDTH }}
title={`${table.number} · ${slot}`}
/>
))}
@@ -88,21 +98,19 @@ export default function Timeline() {
{tableReservations.map((res: Reservation) => {
const startMin = timeToMinutes(res.startTime) - TIMELINE_START;
const endMin = timeToMinutes(res.endTime) - TIMELINE_START;
const leftPct = (startMin / TOTAL_MINUTES) * 100;
const widthPct = ((endMin - startMin) / TOTAL_MINUTES) * 100;
const totalWidth = HOURS.length * COL_WIDTH;
const totalWidth = SLOTS.length * SLOT_WIDTH;
return (
<div
key={res.id}
title={`${res.guest.firstName} ${res.guest.lastName} (${res.guestCount}P) ${res.startTime}${res.endTime}`}
className={`absolute top-2 bottom-2 rounded-lg border px-2 flex items-center overflow-hidden text-xs font-semibold cursor-pointer hover:brightness-110 transition z-10 ${STATUS_BAR[res.status]}`}
className={`absolute top-1.5 bottom-1.5 rounded-md border px-2 flex items-center overflow-hidden text-xs font-semibold cursor-pointer hover:brightness-110 transition z-10 ${STATUS_BAR[res.status]}`}
style={{
left: `${(startMin / TOTAL_MINUTES) * totalWidth}px`,
width: `${((endMin - startMin) / TOTAL_MINUTES) * totalWidth - 4}px`,
width: `${((endMin - startMin) / TOTAL_MINUTES) * totalWidth - 2}px`,
}}
>
<span className={`w-2 h-2 rounded-full shrink-0 mr-1.5 ${STATUS_DOT[res.status]}`} />
<span className={`w-1.5 h-1.5 rounded-full shrink-0 mr-1.5 ${STATUS_DOT[res.status]}`} />
<span className="truncate">{res.guest.firstName} {res.guest.lastName}</span>
</div>
);