ui improvemnt

This commit is contained in:
Marshal
2026-06-06 21:17:20 +00:00
parent b62ea3f95f
commit 08b46c306f
35 changed files with 2731 additions and 1604 deletions

View File

@@ -1,11 +1,155 @@
import FeaturePlaceholder from "@/components/FeaturePlaceholder";
import { useParams, useNavigate } from "react-router-dom";
import { Container, Stack, Grid } from "@mantine/core";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import {
detailStyles,
type BookingDetailView,
BookingDetailToolbar,
BookingDetailHeader,
BookingLifecycleStepper,
BookingRouteCard,
BookingContainersCard,
BookingApprovalCard,
BookingReviewNotesCard,
BookingPaymentCard,
BookingFactsCard,
BookingDocumentsCard,
} from "@/components/bookings/detail";
const BookingDetailPage = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
// Mock data - replace with actual API call
const booking: BookingDetailView = {
id: id || "a61955b7-af21-4664-84d3-7e4e66293b6f",
reference: "BKG-2026-001456",
status: "IN_TRANSIT",
scheduledDate: "2026-06-15",
totalAmount: 15750.5,
paymentCurrency: "USD",
paymentStatus: "PAID",
tradeDirection: "IMPORT",
freightType: "CONTAINER",
priorityScore: 650,
company: {
id: "1",
companyName: "Global Logistics Inc.",
name: "Global Logistics Inc.",
},
originYard: { id: "1", label: "Port of Shanghai", code: "PVG" },
destinationYard: { id: "2", label: "Port of Addis Ababa", code: "AAA" },
serviceType: { id: "1", label: "Container Import Service", code: "CIS" },
cargoType: { id: "1", label: "Electronics", code: "ELEC" },
shippingLine: { id: "1", label: "Maersk Line", code: "MAE" },
cargoTotalWeightVgm: 22.5,
pnrCode: "PNR-2026-001456",
createdAt: "2026-06-05T10:30:00Z",
updatedAt: "2026-06-06T14:20:00Z",
bookingContainers: [
{
id: "1",
quantity: 2,
vgmPerUnitTons: 11.25,
containerType: { label: "20FT Standard", sizeFt: 20, isReefer: false },
},
],
approvalSteps: [
{
id: "1",
stepOrder: 1,
requiredRole: "LINE_STAFF",
status: "APPROVED",
actionedAt: "2026-06-05T11:00:00Z",
},
// {
// id: "2",
// stepOrder: 2,
// requiredRole: "DIRECTOR",
// status: "APPROVED",
// actionedAt: "2026-06-05T13:30:00Z",
// },
{
id: "3",
stepOrder: 3,
requiredRole: "CEO",
status: "APPROVED",
actionedAt: "2026-06-05T15:45:00Z",
},
],
reviewNotes: [
{
id: "1",
note: "Cargo declaration verified against shipping documents.",
type: "VERIFICATION",
createdAt: "2026-06-05T11:15:00Z",
},
{
id: "2",
note: "VGM documentation received and processed.",
type: "COMPLIANCE",
createdAt: "2026-06-05T12:00:00Z",
},
],
files: [
{ id: "1", name: "Bill_of_Lading.pdf", mimeType: "application/pdf" },
{ id: "2", name: "VGM_Certificate.pdf", mimeType: "application/pdf" },
{ id: "3", name: "Commercial_Invoice.pdf", mimeType: "application/pdf" },
],
};
const approvalSteps = booking.approvalSteps ?? [];
const approvedCount = approvalSteps.filter((s) => s.status === "APPROVED").length;
const totalSteps = approvalSteps.length;
return (
<FeaturePlaceholder
title="Booking Detail"
description="Inspect booking metadata, operational notes, and fulfillment progress for internal teams."
/>
<div style={detailStyles.page}>
<Container size="xxl" py="lg">
<BookingDetailToolbar onBack={() => navigate(-1)} />
<Breadcrumbs
items={[
{ label: "Operations" },
{ label: "Booking requests", href: "/dashboard/booking-requests" },
{ label: booking.reference },
]}
/>
{/*
<BookingDetailHeader
booking={booking}
approvedCount={approvedCount}
totalSteps={totalSteps}
/> */}
<BookingLifecycleStepper status={booking.status} />
<Grid gutter="lg">
{/* LEFT — primary content */}
<Grid.Col span={{ base: 12, lg: 8 }}>
<Stack gap="lg">
<BookingRouteCard booking={booking} />
<BookingContainersCard containers={booking.bookingContainers ?? []} />
<BookingApprovalCard steps={approvalSteps} approvedCount={approvedCount} />
<BookingReviewNotesCard notes={booking.reviewNotes ?? []} />
</Stack>
</Grid.Col>
{/* RIGHT — summary sidebar */}
<Grid.Col span={{ base: 12, lg: 4 }}>
<Stack gap="lg">
<BookingPaymentCard
totalAmount={booking.totalAmount}
currency={booking.paymentCurrency}
paymentStatus={booking.paymentStatus}
/>
<BookingFactsCard booking={booking} />
<BookingDocumentsCard files={booking.files ?? []} />
</Stack>
</Grid.Col>
</Grid>
</Container>
</div>
);
};

View File

@@ -1,111 +1,110 @@
import { useNavigate, useParams } from "react-router-dom";
import { ArrowLeft, FileSignature, Package } from "lucide-react";
import {
Anchor,
ArrowLeft,
ArrowRight,
Building2,
Calendar,
Clock,
Loader2,
MapPin,
Package,
FileSignature,
RefreshCw,
Train,
Truck,
} from "lucide-react";
Container,
Stack,
Grid,
Center,
Loader,
Text,
Paper,
Button,
Box,
} from "@mantine/core";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { ApprovalStepsCard } from "@/components/bookings/ApprovalStepsCard";
import { NextStepBanner } from "@/components/bookings/NextStepBanner";
import { BookingActionsToolbar } from "@/components/bookings/BookingActionsToolbar";
import { BookingPricingSummary } from "@/components/bookings/BookingPricingSummary";
import { BookingPriorityBadge } from "@/components/bookings/BookingPriorityBadge";
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
import { BookingWorkflowStepper } from "@/components/bookings/BookingWorkflowStepper";
import {
bookingGlass,
bookingSurface,
} from "@/components/bookings/booking-ui.styles";
detailStyles,
BookingRequestHero,
BookingRouteServiceCard,
BookingMileServicesCard,
BookingCargoCard,
BookingContractSummaryCard,
} from "@/components/bookings/detail";
import { getStatusMeta } from "@/features/bookings/booking-status.config";
import { toBookingListRow } from "@/features/bookings/mapBookingListRow";
import {
useBookingDetail,
useBookingMutations,
} from "@/hooks/bookings/useBookings";
import type { BookingDetail } from "@/types/booking";
import { cn } from "@/lib/utils";
import {
Badge,
Button,
Separator,
} from "@edr/ui-common";
import { useBookingDetail, useBookingMutations } from "@/hooks/bookings/useBookings";
export default function BookingRequestDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { data: booking, isLoading, isError, refetch, isFetching } =
useBookingDetail(id);
const { data: booking, isLoading, isError, refetch, isFetching } = useBookingDetail(id);
const mutations = useBookingMutations(id ?? "");
if (isLoading) {
return (
<div className={bookingSurface.page}>
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4 p-8">
<Loader2 className="size-10 animate-spin text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">
Loading booking
</p>
</div>
</div>
<Box style={detailStyles.page}>
<Center mih="60vh">
<Stack align="center" gap="md">
<Loader color="green" />
<Text size="sm" c="dimmed" fw={500}>
Loading booking
</Text>
</Stack>
</Center>
</Box>
);
}
if (isError || !booking) {
return (
<div className={bookingSurface.page}>
<div className={bookingSurface.pageInner}>
<div
className={cn(
bookingSurface.sectionCard,
"mx-auto max-w-md p-12 text-center",
)}
>
<div
className={cn(
"mx-auto flex size-16 items-center justify-center rounded-2xl",
bookingGlass.iconWellGreen,
)}
>
<Package className="size-8" />
</div>
<h1 className="mt-6 text-xl font-bold text-foreground">
<Box style={detailStyles.page}>
<Container size="sm" py="xl">
<Paper radius="md" withBorder p="xl" ta="center" style={detailStyles.card}>
<Center>
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 64,
height: 64,
borderRadius: 16,
background: "var(--mantine-color-green-0)",
color: "var(--mantine-color-green-7)",
}}
>
<Package size={32} />
</Box>
</Center>
<Text fw={700} size="lg" mt="lg">
Booking not found
</h1>
<p className="mt-2 text-sm text-muted-foreground">
</Text>
<Text size="sm" c="dimmed" mt={4}>
This request may have been removed or the link is invalid.
</p>
</Text>
<Button
className="mt-6 gap-2"
variant="outline"
variant="default"
mt="lg"
leftSection={<ArrowLeft size={16} />}
onClick={() => navigate("/dashboard/booking-requests")}
>
<ArrowLeft className="size-4" />
Back to booking requests
</Button>
</div>
</div>
</div>
</Paper>
</Container>
</Box>
);
}
const row = toBookingListRow(booking);
const statusMeta = getStatusMeta(booking.status);
const amount = Number(booking.totalAmount);
const showContractButton = [
"CONTRACT_READY",
"SIGNED_CUSTOMER",
"FULLY_EXECUTED",
].includes(booking.status);
const showApprovalCard =
booking.status === "PENDING_APPROVAL" ||
booking.status === "APPROVED_PENDING_SIGNATURE";
return (
<div className={bookingSurface.page}>
<div className={bookingSurface.pageInner}>
<Box style={detailStyles.page}>
<Container size="xxl" py="lg">
<Breadcrumbs
items={[
{ label: "Booking requests", href: "/dashboard/booking-requests" },
@@ -113,381 +112,66 @@ export default function BookingRequestDetailPage() {
]}
/>
<div className={bookingSurface.detailHero}>
<div className={bookingSurface.heroGlow} />
<div className={bookingSurface.heroSheen} />
<div className="relative p-6 sm:p-8">
<div className="mb-4">
<Button
variant="ghost"
size="sm"
className="-ml-2 gap-2 text-muted-foreground hover:text-foreground"
onClick={() => navigate("/dashboard/booking-requests")}
>
<ArrowLeft className="size-4" />
Back to list
</Button>
</div>
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
<div className="flex gap-4">
<div
className={cn(
"flex size-16 shrink-0 items-center justify-center rounded-2xl",
bookingGlass.iconWellGreen,
<Stack gap="lg" mt="sm">
<BookingRequestHero
booking={booking}
customerLabel={row.customerLabel}
onBack={() => navigate("/dashboard/booking-requests")}
onRefresh={() => refetch()}
isFetching={isFetching}
/>
<BookingWorkflowStepper
status={booking.status}
title={statusMeta.title}
description={statusMeta.description}
titleColor={statusMeta.color}
/>
<Grid gutter="lg">
{/* LEFT — primary content */}
<Grid.Col span={{ base: 12, lg: 8 }}>
<Stack gap="lg">
<BookingRouteServiceCard
booking={booking}
originLabel={row.originLabel}
destinationLabel={row.destinationLabel}
/>
<BookingMileServicesCard booking={booking} />
<BookingCargoCard booking={booking} />
{booking.contractSummary && (
<BookingContractSummaryCard summary={booking.contractSummary} />
)}
</Stack>
</Grid.Col>
{/* RIGHT — sticky action / summary rail */}
<Grid.Col span={{ base: 12, lg: 4 }}>
<Box style={{ position: "sticky", top: 24 }}>
<Stack gap="lg">
<BookingPricingSummary booking={booking} />
<BookingActionsToolbar booking={booking} mutations={mutations} />
{showContractButton && (
<Button
fullWidth
color="green"
leftSection={<FileSignature size={16} />}
onClick={() =>
navigate(`/dashboard/booking-requests/${booking.id}/contract`)
}
>
View & sign contract
</Button>
)}
>
<Package className="size-7" strokeWidth={1.75} />
</div>
<div className="min-w-0 space-y-3">
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
Booking reference
</p>
<div className="flex flex-wrap items-center gap-2">
<h1 className="text-2xl font-semibold tracking-tight text-foreground sm:text-[1.75rem]">
{booking.reference}
</h1>
<BookingStatusBadge status={booking.status} />
<BookingPriorityBadge score={booking.priorityScore} />
</div>
{booking.nextStep && (
<NextStepBanner nextStep={booking.nextStep} className="max-w-xl" />
{showApprovalCard && (
<ApprovalStepsCard booking={booking} mutations={mutations} />
)}
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground">
<span className="inline-flex items-center gap-1.5 font-medium text-foreground">
<Building2 className="size-4 opacity-70" />
{row.customerLabel}
</span>
<span className="inline-flex items-center gap-1.5">
<Calendar className="size-4 opacity-70" />
Scheduled {booking.scheduledDate}
</span>
<span className="inline-flex items-center gap-1.5">
<Clock className="size-4 opacity-70" />
Created{" "}
{new Date(booking.createdAt).toLocaleDateString(undefined, {
dateStyle: "medium",
})}
</span>
</div>
</div>
</div>
<div className="flex flex-col items-stretch gap-3 sm:items-end">
<div className={cn(bookingSurface.valueCard, "min-w-[12rem] text-right")}>
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
Total value
</p>
<p className="mt-1 font-mono text-2xl font-semibold tabular-nums text-foreground">
{booking.paymentCurrency}{" "}
{amount.toLocaleString(undefined, {
minimumFractionDigits: 2,
})}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{booking.paymentStatus}
</p>
</div>
<Button
variant="outline"
size="sm"
className="gap-2 self-end border-border/60 bg-background/60 backdrop-blur-sm hover:bg-background/80"
disabled={isFetching}
onClick={() => refetch()}
>
<RefreshCw
className={cn("size-4", isFetching && "animate-spin")}
/>
Refresh
</Button>
</div>
</div>
</div>
</div>
<BookingWorkflowStepper
status={booking.status}
title={statusMeta.title}
description={statusMeta.description}
titleColor={statusMeta.color}
/>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-12 xl:gap-8">
<div className="flex flex-col gap-6 xl:col-span-8">
<RouteCard booking={booking} row={row} />
<MileCard booking={booking} />
<CargoCard booking={booking} />
{booking.contractSummary && (
<SectionShell
icon={<Anchor className="size-4" />}
title="Contract summary"
subtitle="Generated terms"
>
<pre className="max-h-64 overflow-auto whitespace-pre-wrap rounded-lg border border-border/50 bg-muted/10 p-4 font-mono text-xs leading-relaxed text-muted-foreground backdrop-blur-sm">
{booking.contractSummary}
</pre>
</SectionShell>
)}
</div>
<div
className={cn(
"flex flex-col gap-6",
bookingSurface.stickySidebar,
"xl:col-span-4",
)}
>
<BookingPricingSummary booking={booking} />
<BookingActionsToolbar booking={booking} mutations={mutations} />
{["CONTRACT_READY", "SIGNED_CUSTOMER", "FULLY_EXECUTED"].includes(
booking.status,
) && (
<div className={bookingSurface.sectionCard}>
<div className="px-5 py-4">
<Button
className="w-full gap-2 shadow-sm"
variant="default"
onClick={() =>
navigate(`/dashboard/booking-requests/${booking.id}/contract`)
}
>
<FileSignature className="size-4" />
View &amp; sign contract
</Button>
</div>
</div>
)}
{(booking.status === "PENDING_APPROVAL" ||
booking.status === "APPROVED_PENDING_SIGNATURE") && (
<ApprovalStepsCard booking={booking} mutations={mutations} />
)}
</div>
</div>
</div>
</div>
);
}
function SectionShell({
icon,
title,
subtitle,
children,
}: {
icon: React.ReactNode;
title: string;
subtitle?: string;
children: React.ReactNode;
}) {
return (
<div className={bookingSurface.sectionCard}>
<div className={bookingSurface.sectionHeader}>
<div className={bookingSurface.sectionIcon}>{icon}</div>
<div>
<h2 className="text-sm font-semibold text-foreground">{title}</h2>
{subtitle && (
<p className="text-xs text-muted-foreground">{subtitle}</p>
)}
</div>
</div>
<div className={bookingSurface.sectionBody}>{children}</div>
</div>
);
}
function RouteCard({
booking,
row,
}: {
booking: BookingDetail;
row: ReturnType<typeof toBookingListRow>;
}) {
return (
<SectionShell
icon={<Train className="size-4" />}
title="Route & service"
subtitle="Corridor and service level"
>
<div
className={cn(
"flex flex-col items-stretch gap-6 rounded-xl border border-dashed border-emerald-500/20 p-5 backdrop-blur-sm md:flex-row md:items-center md:justify-between",
bookingGlass.activeTab,
)}
>
<RouteEndpoint label="Origin" station={row.originLabel} />
<div className="flex flex-col items-center gap-2 px-4">
<div className={cn("flex size-10 items-center justify-center rounded-full", bookingGlass.iconWellGreen)}>
<Train className="size-5 text-black" strokeWidth={1.75} />
</div>
<ArrowRight className="size-5 rotate-90 text-muted-foreground md:rotate-0" />
<Badge
variant="outline"
className="border-border/50 bg-background/50 text-[10px] font-medium uppercase backdrop-blur-sm"
>
{booking.serviceType?.label ??
booking.serviceType?.code ??
"Rail service"}
</Badge>
</div>
<RouteEndpoint label="Destination" station={row.destinationLabel} />
</div>
<div className="mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<MetricTile label="Trade direction" value={booking.tradeDirection} />
<MetricTile label="Freight type" value={booking.freightType} />
<MetricTile
label="Equipment return"
value={booking.equipmentReturn ?? "—"}
/>
{booking.shippingLine && (
<MetricTile
label="Shipping line"
value={
booking.shippingLine.label ??
booking.shippingLine.name ??
booking.shippingLine.code ??
"—"
}
/>
)}
</div>
</SectionShell>
);
}
function MileCard({ booking }: { booking: BookingDetail }) {
if (!booking.firstMilePickupAddress && !booking.lastMileDeliveryAddress) {
return null;
}
return (
<SectionShell
icon={<Truck className="size-4" />}
title="Mile services"
subtitle="First and last mile"
>
<div className="grid gap-4 md:grid-cols-2">
{booking.firstMilePickupAddress && (
<MetricTile
label="First mile pickup"
value={booking.firstMilePickupAddress}
/>
)}
{booking.lastMileDeliveryAddress && (
<MetricTile
label="Last mile delivery"
value={booking.lastMileDeliveryAddress}
/>
)}
</div>
</SectionShell>
);
}
function CargoCard({ booking }: { booking: BookingDetail }) {
const containers = booking.bookingContainers ?? [];
return (
<SectionShell
icon={<Package className="size-4" />}
title="Cargo specifications"
subtitle="Freight and containers"
>
<div className="grid gap-3 sm:grid-cols-3">
<MetricTile
label="Cargo type"
value={booking.cargoType?.label ?? booking.freightType}
/>
<MetricTile
label="Total VGM"
value={`${booking.cargoTotalWeightVgm} tons`}
/>
<MetricTile
label="Hazardous"
value={booking.isHazardous ? "Yes" : "No"}
highlight={booking.isHazardous}
/>
</div>
{containers.length > 0 && (
<>
<Separator className="my-5" />
<div className="overflow-hidden rounded-lg border border-border/50 backdrop-blur-sm">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border/50 bg-muted/20 text-left text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
<th className="px-4 py-3">Container type</th>
<th className="px-4 py-3">Qty</th>
<th className="px-4 py-3">VGM / unit</th>
</tr>
</thead>
<tbody>
{containers.map((c) => (
<tr
key={c.id}
className="border-b border-border/60 last:border-0"
>
<td className="px-4 py-3 font-medium text-foreground">
{c.containerType?.label ??
c.containerType?.code ??
c.containerTypeId}
</td>
<td className="px-4 py-3 tabular-nums text-muted-foreground">
{c.quantity}
</td>
<td className="px-4 py-3 tabular-nums text-muted-foreground">
{c.vgmPerUnitTons} t
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</SectionShell>
);
}
function RouteEndpoint({
label,
station,
}: {
label: string;
station: string;
}) {
return (
<div className="flex min-w-0 items-center gap-3 md:max-w-[14rem]">
<div className={bookingSurface.sectionIconLg}>
<MapPin className="size-5" strokeWidth={1.75} />
</div>
<div className="min-w-0">
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
{label}
</p>
<p className="truncate text-sm font-semibold text-foreground">{station}</p>
</div>
</div>
);
}
function MetricTile({
label,
value,
highlight,
}: {
label: string;
value: string;
highlight?: boolean;
}) {
return (
<div
className={cn(
bookingSurface.metricTile,
highlight && "border-amber-300/50 bg-amber-50/50 dark:bg-amber-950/20",
)}
>
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
{label}
</p>
<p className="mt-1.5 text-sm font-medium leading-snug text-foreground">
{value}
</p>
</div>
</Stack>
</Box>
</Grid.Col>
</Grid>
</Stack>
</Container>
</Box>
);
}

View File

@@ -14,6 +14,20 @@ import {
User,
X,
} from "lucide-react";
import {
Container,
Stack,
Group,
Title,
Text,
Card,
TextInput,
ActionIcon,
Badge as MantineBadge,
Button as MantineButton,
ThemeIcon,
Paper,
} from "@mantine/core";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { BookingPriorityBadge } from "@/components/bookings/BookingPriorityBadge";
@@ -26,12 +40,7 @@ import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
import { BookingApprovalProgressCell } from "@/components/bookings/BookingApprovalProgressCell";
import { BookingActionsMenu } from "@/components/bookings/BookingActionsMenu";
import { BookingTableEmpty } from "@/components/bookings/BookingTableEmpty";
import {
bookingGlass,
bookingInput,
bookingSurface,
bookingTable,
} from "@/components/bookings/booking-ui.styles";
import { bookingTable } from "@/components/bookings/booking-ui.styles";
import { BOOKING_LIST_TABS } from "@/features/bookings/booking-status.config";
import { toBookingListRow } from "@/features/bookings/mapBookingListRow";
import { useBookingList, useBookingListSummary } from "@/hooks/bookings/useBookings";
@@ -237,168 +246,195 @@ export default function BookingRequestsPage() {
];
return (
<div className={bookingSurface.page}>
<div className={bookingSurface.pageInner}>
<div style={{ background: "var(--mantine-color-gray-0)", minHeight: "100vh" }}>
<Container size="xxl" py="xl">
<Breadcrumbs items={[{ label: "Operations" }, { label: "Booking requests" }]} />
<div className={bookingSurface.hero}>
<div className={bookingSurface.heroGlow} />
<div className={bookingSurface.heroSheen} />
<div className="relative flex flex-col gap-6 p-6 sm:flex-row sm:items-center sm:justify-between sm:p-8">
<div className="flex items-start gap-4">
<div
className={cn(
"flex size-14 shrink-0 items-center justify-center rounded-2xl",
bookingGlass.iconWellGreen,
)}
>
<Inbox className="size-6" strokeWidth={1.75} />
</div>
<div>
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
Operations
</p>
<h1 className="mt-1 text-2xl font-semibold tracking-tight text-foreground sm:text-[1.75rem]">
Booking requests
</h1>
<p className="mt-1.5 max-w-xl text-sm leading-relaxed text-muted-foreground">
Track bookings from submission through payment and operations.
</p>
</div>
</div>
<div className="flex shrink-0 items-center gap-2">
<Button
variant="outline"
size="sm"
className="gap-2 border-border/60 bg-background/60 backdrop-blur-sm hover:bg-background/80"
disabled={isFetching}
onClick={handleRefresh}
>
<RefreshCw
className={cn("size-4", isFetching && "animate-spin")}
/>
Refresh
</Button>
</div>
</div>
</div>
<BookingStatGrid
items={[
{
label: "In queue",
value: statValue(metrics?.inQueue),
hint: "Total matching filter",
icon: LayoutList,
},
{
label: "On this page",
value: statValue(metrics?.onThisPage),
hint: "Current page",
icon: FileText,
},
{
label: "Needs action",
value: statValue(metrics?.needsAction),
hint: "Submitted or pending approval",
icon: Clock,
accent:
!summaryLoading && (metrics?.needsAction ?? 0) > 0
? "amber"
: "default",
},
{
label: "Urgent",
value: statValue(metrics?.urgent),
hint: "High priority score",
icon: AlertCircle,
accent:
!summaryLoading && (metrics?.urgent ?? 0) > 0 ? "rose" : "default",
},
]}
/>
<BookingStatusTabs
active={activeTab}
onChange={(tab) => {
setActiveTab(tab);
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
{/*
<Card
p="lg"
radius="lg"
withBorder
mb="xl"
style={{
background: "white",
border: "1px solid var(--mantine-color-gray-2)",
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.05)",
}}
counts={tabCounts}
/>
<div className={bookingSurface.panel}>
<div className={bookingSurface.panelToolbar}>
<div className="relative min-w-[12rem] flex-1 sm:max-w-sm">
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search reference or customer…"
className={bookingInput.search}
/>
{query && (
<button
type="button"
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
onClick={() => setQuery("")}
aria-label="Clear search"
>
<X className="size-3.5" />
</button>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
<span
className={cn(
"hidden rounded-md border border-border/50 bg-background/50 px-2.5 py-1 text-xs text-muted-foreground backdrop-blur-sm sm:inline",
)}
>
<Group justify="space-between" align="flex-start">
<Group gap="md" align="flex-start">
<ThemeIcon
size="lg"
radius="lg"
color="green"
variant="light"
>
{total} record{total !== 1 ? "s" : ""}
</span>
</div>
</div>
<Inbox size={28} />
</ThemeIcon>
<Stack gap={8}>
<Text size="xs" fw={600} c="dimmed" tt="uppercase">
Operations
</Text>
<Title order={1} size="h2">
Booking Requests
</Title>
<Text size="sm" c="dimmed" maw="500px">
Track bookings from submission through payment and operations. Monitor status, prioritize urgent bookings, and manage approvals.
</Text>
</Stack>
</Group>
<MantineButton
variant="light"
color="green"
leftSection={<RefreshCw size={18} />}
disabled={isFetching}
onClick={handleRefresh}
loading={isFetching}
>
Refresh
</MantineButton>
</Group>
</Card> */}
{showEmpty ? (
<BookingTableEmpty
isError={isError}
hasSearch={hasSearch}
onRetry={handleRefresh}
/>
) : (
<div className={bookingSurface.tableWrap}>
<DataTable
columns={columns}
data={rows}
status={isLoading ? "loading" : isError ? "error" : "success"}
onRowClick={handleRowClick}
pagination={{
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
pageCount,
totalCount: total,
<div className="mt-6"></div>
<Stack gap="lg">
<BookingStatGrid
items={[
{
label: "In queue",
value: statValue(metrics?.inQueue),
hint: "Total matching filter",
icon: LayoutList,
},
{
label: "On this page",
value: statValue(metrics?.onThisPage),
hint: "Current page",
icon: FileText,
},
{
label: "Needs action",
value: statValue(metrics?.needsAction),
hint: "Submitted or pending approval",
icon: Clock,
accent:
!summaryLoading && (metrics?.needsAction ?? 0) > 0
? "amber"
: "default",
},
{
label: "Urgent",
value: statValue(metrics?.urgent),
hint: "High priority score",
icon: AlertCircle,
accent:
!summaryLoading && (metrics?.urgent ?? 0) > 0 ? "rose" : "default",
},
]}
/>
<Paper
p="md"
radius="lg"
withBorder
style={{
background: "white",
border: "1px solid var(--mantine-color-gray-2)",
overflowX: "auto",
overflowY: "hidden",
WebkitOverflowScrolling: "touch",
scrollBehavior: "smooth",
}}
>
<div style={{ minWidth: "min-content", display: "inline-block", width: "100%" }}>
<BookingStatusTabs
active={activeTab}
onChange={(tab) => {
setActiveTab(tab);
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
}}
tableOptions={{
state: { pagination },
onPaginationChange: setPagination,
manualPagination: true,
pageCount,
}}
containerClassName={cn(
"border-0 shadow-none",
"[&_thead_tr]:border-b [&_thead_tr]:border-border/50",
"[&_thead_th]:bg-muted/20 [&_thead_th]:backdrop-blur-sm",
"[&_tbody_tr]:group/tr [&_tbody_tr]:cursor-pointer [&_tbody_tr]:border-b [&_tbody_tr]:border-border/30",
"[&_tbody_tr]:transition-colors [&_tbody_tr:hover]:bg-muted/20",
)}
footer={DataTableFooter}
counts={tabCounts}
/>
</div>
)}
</div>
</div>
</Paper>
<Card
p="md"
radius="lg"
withBorder
style={{
background: "white",
border: "1px solid var(--mantine-color-gray-2)",
}}
>
<Stack gap="md">
<Group justify="space-between" gap="md" wrap="wrap">
<TextInput
placeholder="Search reference or customer…"
leftSection={<Search size={18} />}
value={query}
onChange={(e) => setQuery(e.target.value)}
rightSection={
query && (
<ActionIcon
size="sm"
color="gray"
radius="md"
variant="transparent"
onClick={() => setQuery("")}
>
<X size={16} />
</ActionIcon>
)
}
style={{ flex: 1, minWidth: "200px" }}
radius="lg"
/>
<Text size="sm" c="dimmed">
{total} record{total !== 1 ? "s" : ""}
</Text>
</Group>
{showEmpty ? (
<BookingTableEmpty
isError={isError}
hasSearch={hasSearch}
onRetry={handleRefresh}
/>
) : (
<div style={{ overflowX: "auto" }}>
<DataTable
columns={columns}
data={rows}
status={isLoading ? "loading" : isError ? "error" : "success"}
onRowClick={handleRowClick}
pagination={{
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
pageCount,
totalCount: total,
}}
tableOptions={{
state: { pagination },
onPaginationChange: setPagination,
manualPagination: true,
pageCount,
}}
containerClassName={cn(
"border-0 shadow-none",
"[&_thead_tr]:border-b [&_thead_tr]:border-border/50",
"[&_thead_th]:bg-muted/20 [&_thead_th]:backdrop-blur-sm",
"[&_tbody_tr]:group/tr [&_tbody_tr]:cursor-pointer [&_tbody_tr]:border-b [&_tbody_tr]:border-border/30",
"[&_tbody_tr]:transition-colors [&_tbody_tr:hover]:bg-muted/20",
)}
footer={DataTableFooter}
/>
</div>
)}
</Stack>
</Card>
</Stack>
</Container>
</div>
);
}