feat(api): add REST controllers, services, entities and 15-min timeline grid
This commit is contained in:
20
frontend/src/api/client.ts
Normal file
20
frontend/src/api/client.ts
Normal 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),
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user