import { useCallback, useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { api } from "../api/client"; import type { Booking, RegisterReturnResult, VehicleDetail } from "../api/types"; import { useDemoManifest } from "../context/DemoManifestContext"; import { useLocaleFormat } from "../i18n/format"; import { StatusBadge } from "../components/Badge"; import { ReturnForm, ReturnResultPanel } from "../components/ReturnForm"; import { Icon } from "../components/Icons"; import { ErrorState, LoadingState, PageHeader } from "../components/PageChrome"; import { PRODUCT_NAME } from "../product"; export function BookingDetail() { const { t } = useTranslation(["bookings", "returns"]); const { formatDateTime, formatNumber } = useLocaleFormat(); const { publicRef } = useParams<{ publicRef: string }>(); const { manifest } = useDemoManifest(); const [booking, setBooking] = useState(null); const [error, setError] = useState(null); const [returnResult, setReturnResult] = useState(null); const [canonicalOdometerKm, setCanonicalOdometerKm] = useState(null); const load = useCallback(() => { if (!publicRef) return; api .get(`/api/v1/bookings/${publicRef}`) .then(setBooking) .catch(() => setError(t("detail.notFound"))); }, [publicRef]); useEffect(() => { setBooking(null); setError(null); setReturnResult(null); load(); }, [load]); // The odometer-regression demo scenario supplies its own suspicious reading (per the // brief: never ask a demo visitor to invent one) -- only fetched for that one known // scenario booking, not for every return. const isReturnAnomalyScenario = manifest?.scenarios.find((s) => s.id === "return-anomaly")?.start_path === `/bookings/${publicRef}`; useEffect(() => { setCanonicalOdometerKm(null); if (!isReturnAnomalyScenario || !booking) return; api .get(`/api/v1/vehicles/${booking.vehicle_ref}`) .then((vehicle) => setCanonicalOdometerKm(vehicle.odometer_km)) .catch(() => setCanonicalOdometerKm(null)); }, [isReturnAnomalyScenario, booking]); function handleRegistered(result: RegisterReturnResult) { setReturnResult(result); load(); } if (error) return ; if (!booking) return ; return (
{t("detail.backLink")} } />
{t("detail.customer")}
{booking.customer_name} ({booking.customer_ref})
{t("detail.vehicle")}
{booking.vehicle_ref}
{t("detail.starts")}
{formatDateTime(booking.starts_at)}
{t("detail.ends")}
{formatDateTime(booking.ends_at)}
{t("detail.startOdometer")}
{booking.start_odometer_km !== null ? `${formatNumber(booking.start_odometer_km)} km` : "—"}
{t("detail.endOdometer")}
{booking.end_odometer_km !== null ? `${formatNumber(booking.end_odometer_km)} km` : "—"}
{t("detail.requirementsComplete")}
{booking.requirements_complete ? t("detail.yes") : t("detail.no")}
{isReturnAnomalyScenario && !returnResult && canonicalOdometerKm !== null && (
{t("returns:scenario.title")}

{t("returns:scenario.body", { odometer: formatNumber(canonicalOdometerKm), productName: PRODUCT_NAME })}

)} {returnResult && } {/* Wait for the scenario's canonical odometer before mounting the form at all when this is the known demo scenario, so ReturnForm's suggestedOdometerKm is correct from its very first render -- never updated asynchronously after mount, which previously raced with anyone already typing into the field. */} {!returnResult && booking.status === "active" && isReturnAnomalyScenario && canonicalOdometerKm === null && ( )} {!returnResult && booking.status === "active" && (!isReturnAnomalyScenario || canonicalOdometerKm !== null) && ( )}
); }