149 lines
5.2 KiB
TypeScript
149 lines
5.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Link, useParams } from "react-router-dom";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { useAuth } from "@/context/AuthContext";
|
|
import { apiGet, apiPatch } from "@/lib/api";
|
|
import { isLikelyApiHotelBooking, mapApiBookingToBooking } from "@/lib/hotel-adapters";
|
|
import type { Booking } from "@/lib/types";
|
|
import { useAuthStore } from "@/store/authStore";
|
|
import { formatDate, formatDateTime, formatMoney } from "@/lib/format";
|
|
import { roomDisplayName } from "@/lib/room-utils";
|
|
|
|
export function BookingDetailPage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const selectedPropertyId = useAuthStore((s) => s.selectedPropertyId);
|
|
const [b, setB] = useState<Booking | null>(null);
|
|
const [note, setNote] = useState("");
|
|
const { canEditBookings } = useAuth();
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
apiGet<unknown>(`/bookings/${id}`)
|
|
.then((raw) => {
|
|
if (isLikelyApiHotelBooking(raw)) setB(mapApiBookingToBooking(raw));
|
|
else setB(raw as Booking);
|
|
})
|
|
.catch(console.error);
|
|
}, [id, selectedPropertyId]);
|
|
|
|
if (!b) return <p className="text-muted-foreground">Loading…</p>;
|
|
|
|
async function addNote() {
|
|
if (!b || !note.trim() || !canEditBookings) return;
|
|
const next = await apiPatch<unknown>(`/bookings/${b.id}`, {
|
|
internalNotes: [note.trim()],
|
|
});
|
|
setB(
|
|
isLikelyApiHotelBooking(next)
|
|
? mapApiBookingToBooking(next)
|
|
: (next as Booking)
|
|
);
|
|
setNote("");
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<Button variant="ghost" size="sm" asChild>
|
|
<Link to="/bookings">← Back</Link>
|
|
</Button>
|
|
<h1 className="mt-2 text-2xl font-bold">
|
|
{b.guest.firstName} {b.guest.lastName}
|
|
</h1>
|
|
<p className="text-muted-foreground text-sm">{b.id}</p>
|
|
</div>
|
|
<Badge>{b.status}</Badge>
|
|
</div>
|
|
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
<Card className="rounded-2xl">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Guest</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-1 text-sm">
|
|
<p>
|
|
<span className="text-muted-foreground">Email:</span>{" "}
|
|
{b.guest.email}
|
|
</p>
|
|
<p>
|
|
<span className="text-muted-foreground">Phone:</span>{" "}
|
|
{b.guest.phone}
|
|
</p>
|
|
<p>
|
|
<span className="text-muted-foreground">PNR:</span>{" "}
|
|
{b.guest.flightBookingNumber}
|
|
</p>
|
|
<p>
|
|
<span className="text-muted-foreground">Arrival:</span>{" "}
|
|
{b.guest.arrivalTime}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="rounded-2xl">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Stay</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-1 text-sm">
|
|
<p>
|
|
{formatDate(b.checkIn)} → {formatDate(b.checkOut)} ({b.nights}{" "}
|
|
nights)
|
|
</p>
|
|
<p>Guests: {b.guests}</p>
|
|
<p>Room: {b.roomDisplayLabel ?? roomDisplayName(b.roomId)}</p>
|
|
{b.holdReference && <p>Hold: {b.holdReference}</p>}
|
|
{b.confirmationId && <p>Payment ref: {b.confirmationId}</p>}
|
|
{b.paidAt && <p>Paid: {formatDateTime(b.paidAt)}</p>}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="rounded-2xl lg:col-span-2">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Pricing</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-2 text-sm sm:grid-cols-2">
|
|
<p>Nightly subtotal: {formatMoney(b.pricing.nightlySubtotal)}</p>
|
|
<p>Coupon: {b.pricing.couponCode ?? "—"}</p>
|
|
<p>Discount: {b.pricing.discountPercent}%</p>
|
|
<p>Tax: {formatMoney(b.pricing.taxAmount)}</p>
|
|
<p className="font-semibold sm:col-span-2">
|
|
Total: {formatMoney(b.pricing.total)}
|
|
</p>
|
|
<p>Referral: {b.referralCode ?? "—"}</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="rounded-2xl lg:col-span-2">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Internal notes</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<ul className="list-inside list-disc text-sm text-muted-foreground">
|
|
{(b.internalNotes ?? []).map((n, i) => (
|
|
<li key={i}>{n}</li>
|
|
))}
|
|
</ul>
|
|
{canEditBookings && (
|
|
<>
|
|
<Textarea
|
|
placeholder="Add note…"
|
|
value={note}
|
|
onChange={(e) => setNote(e.target.value)}
|
|
/>
|
|
<Button size="sm" onClick={addNote}>
|
|
Save note
|
|
</Button>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|