204 lines
7.0 KiB
TypeScript
204 lines
7.0 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Link, useSearchParams } from "react-router-dom";
|
|
|
|
import {
|
|
Breadcrumb,
|
|
BreadcrumbItem,
|
|
BreadcrumbLink,
|
|
BreadcrumbList,
|
|
BreadcrumbPage,
|
|
BreadcrumbSeparator,
|
|
} from "@/components/ui/breadcrumb";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { apiGet } from "@/lib/api";
|
|
import type { Booking } from "@/lib/types";
|
|
import { formatDate, formatMoney } from "@/lib/format";
|
|
import { roomDisplayName } from "@/lib/room-utils";
|
|
|
|
export function BookingsPage() {
|
|
const [searchParams] = useSearchParams();
|
|
const ref = searchParams.get("referral") ?? "";
|
|
const [list, setList] = useState<Booking[]>([]);
|
|
const [status, setStatus] = useState<string>("all");
|
|
const [q, setQ] = useState("");
|
|
|
|
useEffect(() => {
|
|
const params = new URLSearchParams();
|
|
if (status !== "all") params.set("status", status);
|
|
if (q) params.set("q", q);
|
|
if (ref) params.set("referralCode", ref);
|
|
const t = setTimeout(() => {
|
|
apiGet<{ data: Booking[] }>(`/bookings?${params}`)
|
|
.then((r) => setList(r.data))
|
|
.catch(console.error);
|
|
}, 200);
|
|
return () => clearTimeout(t);
|
|
}, [status, q, ref]);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Breadcrumb>
|
|
<BreadcrumbList>
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink href="/dashboard">Home</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
<BreadcrumbSeparator />
|
|
<BreadcrumbItem>
|
|
<BreadcrumbPage>Bookings</BreadcrumbPage>
|
|
</BreadcrumbItem>
|
|
</BreadcrumbList>
|
|
</Breadcrumb>
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Bookings</h1>
|
|
<p className="text-muted-foreground text-sm">
|
|
Search, filter, export
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button asChild>
|
|
<Link to="/bookings/new">+ New booking</Link>
|
|
</Button>
|
|
<Button variant="outline" asChild>
|
|
<a href="/api/export/bookings.csv" download>
|
|
Export CSV
|
|
</a>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
{["Total", "Confirmed", "Held", "Pending"].map((label, i) => (
|
|
<Card key={label} className="rounded-2xl">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
{label}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-2xl font-bold">
|
|
{i === 0
|
|
? list.length
|
|
: list.filter((b) =>
|
|
label === "Confirmed"
|
|
? b.status === "confirmed"
|
|
: label === "Held"
|
|
? b.status === "held"
|
|
: b.status === "payment_pending"
|
|
).length}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
<Card className="rounded-2xl">
|
|
<CardContent className="pt-6">
|
|
<div className="mb-4 flex flex-col gap-3 sm:flex-row">
|
|
<Input
|
|
placeholder="Search guest, ref…"
|
|
value={q}
|
|
onChange={(e) => setQ(e.target.value)}
|
|
className="max-w-md"
|
|
/>
|
|
<Select value={status} onValueChange={setStatus}>
|
|
<SelectTrigger className="w-full sm:w-44">
|
|
<SelectValue placeholder="Status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All status</SelectItem>
|
|
<SelectItem value="confirmed">Confirmed</SelectItem>
|
|
<SelectItem value="held">Held</SelectItem>
|
|
<SelectItem value="payment_pending">Payment pending</SelectItem>
|
|
<SelectItem value="cancelled">Cancelled</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="hidden md:block overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Guest</TableHead>
|
|
<TableHead>Room</TableHead>
|
|
<TableHead>Dates</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead className="text-right">Total</TableHead>
|
|
<TableHead />
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{list.map((b) => (
|
|
<TableRow key={b.id}>
|
|
<TableCell>
|
|
<p className="font-medium">
|
|
{b.guest.firstName} {b.guest.lastName}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{b.guest.email}
|
|
</p>
|
|
</TableCell>
|
|
<TableCell className="text-sm">
|
|
{roomDisplayName(b.roomId)}
|
|
</TableCell>
|
|
<TableCell className="text-xs text-muted-foreground">
|
|
{formatDate(b.checkIn)} → {formatDate(b.checkOut)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="secondary">{b.status}</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-right font-medium">
|
|
{formatMoney(b.pricing.total)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button variant="outline" size="sm" asChild>
|
|
<Link to={`/bookings/${b.id}`}>Details</Link>
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
<div className="space-y-3 md:hidden">
|
|
{list.map((b) => (
|
|
<Link
|
|
key={b.id}
|
|
to={`/bookings/${b.id}`}
|
|
className="block rounded-xl border p-4"
|
|
>
|
|
<p className="font-medium">
|
|
{b.guest.firstName} {b.guest.lastName}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{formatDate(b.checkIn)} · {b.status}
|
|
</p>
|
|
<p className="mt-2 font-semibold">
|
|
{formatMoney(b.pricing.total)}
|
|
</p>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|