140 lines
4.5 KiB
TypeScript
140 lines
4.5 KiB
TypeScript
import { format } from "date-fns";
|
|
import { useEffect, useState } from "react";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { apiGet } from "@/lib/api";
|
|
import type { Room } from "@/lib/types";
|
|
|
|
interface TimelineResp {
|
|
days: string[];
|
|
rooms: Room[];
|
|
segments: {
|
|
bookingId: string;
|
|
guestName: string;
|
|
roomId: string;
|
|
start: string;
|
|
end: string;
|
|
status: string;
|
|
paymentLabel: string;
|
|
source: string;
|
|
}[];
|
|
}
|
|
|
|
export function ReservationsPage() {
|
|
const [month, setMonth] = useState(format(new Date(), "yyyy-MM"));
|
|
const [data, setData] = useState<TimelineResp | null>(null);
|
|
|
|
useEffect(() => {
|
|
apiGet<TimelineResp>(`/reservations/timeline?month=${month}`)
|
|
.then(setData)
|
|
.catch(console.error);
|
|
}, [month]);
|
|
|
|
if (!data)
|
|
return <p className="text-muted-foreground">Loading timeline…</p>;
|
|
|
|
const dayWidth = 56;
|
|
const roomCol = 120;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Reservations</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
Gantt-style view (mock data)
|
|
</p>
|
|
</div>
|
|
<input
|
|
type="month"
|
|
value={month}
|
|
onChange={(e) => setMonth(e.target.value)}
|
|
className="rounded-xl border border-input bg-background px-3 py-2 text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<Badge>Occupied</Badge>
|
|
<Badge variant="secondary">Check-in / out</Badge>
|
|
<Badge variant="outline">Reserved</Badge>
|
|
</div>
|
|
|
|
<Card className="rounded-2xl">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Timeline</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="overflow-x-auto">
|
|
<div
|
|
className="relative min-w-max"
|
|
style={{
|
|
width: roomCol + data.days.length * dayWidth,
|
|
}}
|
|
>
|
|
<div className="flex border-b">
|
|
<div
|
|
className="shrink-0 border-r p-2 text-xs font-medium"
|
|
style={{ width: roomCol }}
|
|
>
|
|
Room
|
|
</div>
|
|
{data.days.map((d) => (
|
|
<div
|
|
key={d}
|
|
className="shrink-0 border-r p-1 text-center text-[10px] text-muted-foreground"
|
|
style={{ width: dayWidth }}
|
|
>
|
|
{d.slice(8)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
{data.rooms.map((room) => (
|
|
<div key={room.id} className="flex border-b">
|
|
<div
|
|
className="shrink-0 border-r p-2 text-xs font-medium"
|
|
style={{ width: roomCol }}
|
|
>
|
|
{room.name}
|
|
</div>
|
|
<div
|
|
className="relative shrink-0"
|
|
style={{ width: data.days.length * dayWidth, height: 48 }}
|
|
>
|
|
{data.segments
|
|
.filter((s) => s.roomId === room.id)
|
|
.map((s) => {
|
|
const startIdx = data.days.findIndex(
|
|
(d) => d >= s.start
|
|
);
|
|
const endIdx = data.days.findIndex((d) => d >= s.end);
|
|
const si =
|
|
startIdx >= 0 ? startIdx : 0;
|
|
const ei =
|
|
endIdx >= 0 ? endIdx : data.days.length;
|
|
const span = Math.max(1, ei - si);
|
|
return (
|
|
<div
|
|
key={s.bookingId}
|
|
className="absolute top-2 flex h-8 items-center rounded-lg border bg-accent px-2 text-[10px] shadow-sm"
|
|
style={{
|
|
left: si * dayWidth + 4,
|
|
width: span * dayWidth - 8,
|
|
}}
|
|
title={`${s.guestName} · ${s.paymentLabel}`}
|
|
>
|
|
<span className="truncate font-medium">
|
|
{s.guestName}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|