mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 03:38:17 +00:00
style: rebuild the my bookings page with new ui
This commit is contained in:
@@ -1,32 +1,19 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Badge,
|
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Group,
|
Group,
|
||||||
Menu,
|
Menu,
|
||||||
SimpleGrid,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import { ArrowUpDown, Download, Filter, MoreVertical, Package, Plus } from "lucide-react";
|
||||||
ArrowRight,
|
|
||||||
Clock,
|
|
||||||
Eye,
|
|
||||||
Filter,
|
|
||||||
MoreHorizontal,
|
|
||||||
Package,
|
|
||||||
Plus,
|
|
||||||
Search,
|
|
||||||
Truck,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import type { Freight } from "@edr/types";
|
import type { Freight } from "@edr/types";
|
||||||
@@ -37,63 +24,188 @@ import {
|
|||||||
usePagination,
|
usePagination,
|
||||||
} from "@edr/ui-common";
|
} from "@edr/ui-common";
|
||||||
|
|
||||||
|
// ── Status badge ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const STATUS_CONFIG: Record<string, { bg: string; dot: string; color: string; label: string }> = {
|
||||||
|
DRAFT: { bg: "#F1F4F7", dot: "#94A3B8", color: "#475569", label: "Draft" },
|
||||||
|
REVIEWING: { bg: "#E9F0F8", dot: "#3B6FB0", color: "#2E5B96", label: "Reviewing" },
|
||||||
|
AWAITING_PAYMENT: { bg: "#FDF3E0", dot: "#F2A516", color: "#9A5B00", label: "Awaiting Payment" },
|
||||||
|
CONFIRMED: { bg: "#ECF6F1", dot: "#0EA371", color: "#0A6F4D", label: "Confirmed" },
|
||||||
|
IN_TRANSIT: { bg: "#ECF6F1", dot: "#0EA371", color: "#0A6F4D", label: "In Transit" },
|
||||||
|
DELIVERED: { bg: "#E9F0F8", dot: "#3B6FB0", color: "#2E5B96", label: "Delivered" },
|
||||||
|
CANCELLED: { bg: "#FBEAE7", dot: "#C0392B", color: "#C0392B", label: "Cancelled" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function StatusBadge({ status }: { status: string }) {
|
||||||
|
const cfg = STATUS_CONFIG[status] ?? {
|
||||||
|
bg: "#F1F4F7",
|
||||||
|
dot: "#94A3B8",
|
||||||
|
color: "#475569",
|
||||||
|
label: status.replace(/_/g, " "),
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
gap={6}
|
||||||
|
align="center"
|
||||||
|
wrap="nowrap"
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: cfg.bg,
|
||||||
|
padding: "5px 11px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: cfg.dot,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text fz={11} fw={700} style={{ color: cfg.color, whiteSpace: "nowrap" }}>
|
||||||
|
{cfg.label}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Context-sensitive action button ───────────────────────────────────────────
|
||||||
|
|
||||||
|
function PrimaryAction({
|
||||||
|
status,
|
||||||
|
id,
|
||||||
|
onNavigate,
|
||||||
|
}: {
|
||||||
|
status: string;
|
||||||
|
id: string;
|
||||||
|
onNavigate: (path: string) => void;
|
||||||
|
}) {
|
||||||
|
if (status === "DRAFT") {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
fw={700}
|
||||||
|
fz={13}
|
||||||
|
style={{ backgroundColor: "var(--mantine-color-edr-ink-0)", color: "#fff" }}
|
||||||
|
onClick={() => onNavigate(`/bookings/${id}`)}
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status === "AWAITING_PAYMENT") {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
fw={700}
|
||||||
|
fz={13}
|
||||||
|
style={{ backgroundColor: "var(--mantine-color-edr-accent-0)", color: "#fff" }}
|
||||||
|
onClick={() => onNavigate(`/bookings/${id}`)}
|
||||||
|
>
|
||||||
|
Pay
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status === "IN_TRANSIT") {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="default"
|
||||||
|
fw={600}
|
||||||
|
fz={13}
|
||||||
|
onClick={() => onNavigate(`/bookings/${id}`)}
|
||||||
|
>
|
||||||
|
Track
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="default"
|
||||||
|
fw={600}
|
||||||
|
fz={13}
|
||||||
|
onClick={() => onNavigate(`/bookings/${id}`)}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Column header label ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function ColHeader({ label }: { label: string }) {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
fz={11}
|
||||||
|
fw={700}
|
||||||
|
c="edr-muted"
|
||||||
|
style={{ letterSpacing: "0.6px", textTransform: "uppercase", whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hMeta = { headerClassName: "bg-[#F4F7FA]" };
|
||||||
|
|
||||||
|
// ── Main component ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function MyBookings() {
|
export default function MyBookings() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
|
|
||||||
const { data, isLoading, isError } = useQuery(
|
|
||||||
api.bookings.list.queryOptions(),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery(api.bookings.list.queryOptions());
|
||||||
const bookings = data?.items ?? [];
|
const bookings = data?.items ?? [];
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const total = bookings.length;
|
||||||
return bookings.filter((b) => {
|
|
||||||
const term = searchTerm.toLowerCase();
|
|
||||||
return (
|
|
||||||
b.reference.toLowerCase().includes(term) ||
|
|
||||||
(b.originYard?.label ?? b.originYard?.code ?? "").toLowerCase().includes(term) ||
|
|
||||||
(b.destinationYard?.label ?? b.destinationYard?.code ?? "").toLowerCase().includes(term) ||
|
|
||||||
b.status.toLowerCase().includes(term)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [bookings, searchTerm]);
|
|
||||||
|
|
||||||
const total = filteredData.length;
|
|
||||||
const pageCount = Math.ceil(total / pagination.pageSize);
|
const pageCount = Math.ceil(total / pagination.pageSize);
|
||||||
const start = pagination.pageIndex * pagination.pageSize;
|
const start = pagination.pageIndex * pagination.pageSize;
|
||||||
const end = Math.min(start + pagination.pageSize, total);
|
const end = Math.min(start + pagination.pageSize, total);
|
||||||
|
const paginatedData = useMemo(() => bookings.slice(start, end), [bookings, start, end]);
|
||||||
const paginatedData = useMemo(() => filteredData.slice(start, end), [filteredData, start, end]);
|
|
||||||
|
|
||||||
const activeCount = useMemo(() => {
|
|
||||||
return bookings.filter(
|
|
||||||
(b) => b.status === "CONFIRMED" || b.status === "IN_TRANSIT",
|
|
||||||
).length;
|
|
||||||
}, [bookings]);
|
|
||||||
|
|
||||||
const pendingCount = useMemo(() => {
|
|
||||||
return bookings.filter((b) => b.status === "DRAFT").length;
|
|
||||||
}, [bookings]);
|
|
||||||
|
|
||||||
const columns: ColumnDef<Freight.IBooking>[] = [
|
const columns: ColumnDef<Freight.IBooking>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "reference",
|
id: "booking",
|
||||||
header: "Reference",
|
size: 244,
|
||||||
|
meta: hMeta,
|
||||||
|
header: () => <ColHeader label="Booking" />,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const booking = row.original;
|
const b = row.original;
|
||||||
|
const cargoLabel =
|
||||||
|
b.freightType === "BULK"
|
||||||
|
? "Bulk Cargo"
|
||||||
|
: b.freightType === "BREAK_BULK"
|
||||||
|
? "Break Bulk"
|
||||||
|
: "Cargo";
|
||||||
return (
|
return (
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap={12} wrap="nowrap" align="center">
|
||||||
<Box className="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-700 text-white shadow-sm shadow-emerald-500/30">
|
<Box
|
||||||
<Package className="h-5 w-5" />
|
style={{
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 9,
|
||||||
|
flexShrink: 0,
|
||||||
|
backgroundColor: "var(--mantine-color-edr-soft-0)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Package size={18} color="var(--mantine-color-edr-green-7)" strokeWidth={2} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box style={{ minWidth: 0 }}>
|
||||||
<Text fw={600} size="sm">
|
<Text fz={14} fw={700} c="edr-text" truncate>
|
||||||
{booking.reference}
|
{b.reference}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" c="dimmed">
|
<Text fz={12} c="edr-muted">
|
||||||
{booking.scheduledDate ?? booking.createdAt}
|
{cargoLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -102,72 +214,80 @@ export default function MyBookings() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "route",
|
id: "route",
|
||||||
header: "Route",
|
size: 196,
|
||||||
cell: ({ row }) => (
|
meta: hMeta,
|
||||||
<Group gap={6} wrap="nowrap">
|
header: () => <ColHeader label="Route" />,
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{row.original.originYard?.label ?? row.original.originYard?.code ?? "—"}
|
|
||||||
</Text>
|
|
||||||
<ArrowRight className="h-4 w-4 text-[var(--mantine-color-gray-4)]" />
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{row.original.destinationYard?.label ?? row.original.destinationYard?.code ?? "—"}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cargo",
|
|
||||||
header: "Cargo",
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const b = row.original;
|
const b = row.original;
|
||||||
const containerCount = b.containers?.reduce((sum, c) => sum + c.qty, 0) ?? 0;
|
const origin = b.originYard?.label ?? b.originYard?.code ?? "—";
|
||||||
const containerType = b.containers?.[0]?.type ?? null;
|
const dest = b.destinationYard?.label ?? b.destinationYard?.code ?? "—";
|
||||||
|
const sub = b.scheduledDate ?? b.createdAt ?? "";
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="sm" fw={500}>
|
<Text fz={13} fw={600} c="edr-text">
|
||||||
{b.freightType === "BULK" ? "Bulk" : "Break Bulk"}
|
{origin} → {dest}
|
||||||
</Text>
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
{containerType && containerCount > 0 ? `${containerCount} × ${containerType} · ` : ""}
|
|
||||||
{b.cargoTotalWeightVgm}t
|
|
||||||
</Text>
|
</Text>
|
||||||
|
{sub && (
|
||||||
|
<Text fz={12} c="edr-muted">
|
||||||
|
{sub}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "transportMode",
|
id: "status",
|
||||||
header: "Transport",
|
size: 190,
|
||||||
cell: ({ row }) => (
|
meta: hMeta,
|
||||||
<Text size="sm" c="dimmed">
|
header: () => <ColHeader label="Status" />,
|
||||||
{row.original.serviceType === "RAIL_AND_FORWARDING" ? "Rail & Forwarding" : "Rail"}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "status",
|
|
||||||
header: "Status",
|
|
||||||
cell: ({ row }) => <StatusBadge status={row.original.status} />,
|
cell: ({ row }) => <StatusBadge status={row.original.status} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "amount",
|
||||||
|
size: 140,
|
||||||
|
meta: hMeta,
|
||||||
|
header: () => <ColHeader label="Amount" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const b = row.original as Freight.IBooking & { totalAmount?: number; amount?: number };
|
||||||
|
const amount = b.totalAmount ?? b.amount ?? null;
|
||||||
|
if (!amount) {
|
||||||
|
return (
|
||||||
|
<Text fz={14} fw={700} style={{ color: "#94A3B8" }}>
|
||||||
|
—
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Text fz={14} fw={700} c="edr-text">
|
||||||
|
ETB {amount.toLocaleString()}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
size: 40,
|
meta: hMeta,
|
||||||
|
header: () => null,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const booking = row.original;
|
const booking = row.original;
|
||||||
return (
|
return (
|
||||||
<Group justify="flex-end" onClick={(e) => e.stopPropagation()}>
|
<Group justify="flex-end" gap={8} wrap="nowrap" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<PrimaryAction status={booking.status} id={booking.id} onNavigate={navigate} />
|
||||||
<Menu position="bottom-end" withinPortal shadow="md" radius="md">
|
<Menu position="bottom-end" withinPortal shadow="md" radius="md">
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<ActionIcon variant="default" radius="md" aria-label="Row actions">
|
<ActionIcon
|
||||||
<MoreHorizontal size={16} />
|
variant="transparent"
|
||||||
|
size={30}
|
||||||
|
radius="md"
|
||||||
|
aria-label="More options"
|
||||||
|
>
|
||||||
|
<MoreVertical size={16} color="#9AA8B5" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item
|
<Menu.Item onClick={() => navigate(`/bookings/${booking.id}`)}>
|
||||||
leftSection={<Eye size={15} />}
|
View Details
|
||||||
onClick={() => navigate(`/bookings/${booking.id}`)}
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
@@ -180,111 +300,85 @@ export default function MyBookings() {
|
|||||||
const dataTableStatus = isLoading ? "loading" : isError ? "error" : "success";
|
const dataTableStatus = isLoading ? "loading" : isError ? "error" : "success";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="min-h-screen bg-gray-50/50 p-6">
|
<Box style={{ padding: "28px 32px 32px" }}>
|
||||||
<Stack gap="lg" maw={1280} mx="auto">
|
<Stack gap="lg">
|
||||||
{/* ── Header band ─────────────────────────────────────────── */}
|
{/* ── Page header ─────────────────────────────────────────────── */}
|
||||||
<Card
|
<Group justify="space-between" align="flex-end" wrap="wrap" gap="md">
|
||||||
radius="lg"
|
<Box>
|
||||||
withBorder
|
<Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
|
||||||
className="relative overflow-hidden border-emerald-100! bg-gradient-to-br from-emerald-50 via-white to-white"
|
Bookings
|
||||||
>
|
</Title>
|
||||||
<Box className="pointer-events-none absolute -right-10 -top-16 h-48 w-48 rounded-full bg-emerald-400/10 blur-2xl" />
|
<Text size="sm" c="edr-muted" mt={4}>
|
||||||
<Group justify="space-between" align="flex-end" wrap="wrap" gap="md" className="relative">
|
Manage every cargo booking — from draft to delivery.
|
||||||
<Box>
|
</Text>
|
||||||
<Title order={1} className="tracking-tight">
|
</Box>
|
||||||
My Bookings
|
<Group gap={12}>
|
||||||
</Title>
|
<Button variant="default" radius="md" leftSection={<Download size={16} />}>
|
||||||
<Text size="sm" c="dimmed" mt={4}>
|
Export
|
||||||
View and manage your freight booking requests.
|
</Button>
|
||||||
</Text>
|
<Button
|
||||||
</Box>
|
component={Link}
|
||||||
|
to="/bookings/new"
|
||||||
<Group gap="sm" wrap="wrap">
|
color="edr-green"
|
||||||
<TextInput
|
radius="md"
|
||||||
placeholder="Search bookings..."
|
leftSection={<Plus size={16} />}
|
||||||
value={searchTerm}
|
>
|
||||||
onChange={(e) => setSearchTerm(e.currentTarget.value)}
|
New Booking
|
||||||
leftSection={<Search size={16} />}
|
</Button>
|
||||||
radius="md"
|
|
||||||
className="w-full sm:w-80"
|
|
||||||
styles={{ input: { background: "white" } }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
component={Link}
|
|
||||||
to="/bookings/new"
|
|
||||||
color="edr-green"
|
|
||||||
radius="md"
|
|
||||||
leftSection={<Plus size={16} />}
|
|
||||||
className="shadow-sm shadow-emerald-500/30"
|
|
||||||
>
|
|
||||||
New Booking
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Group>
|
||||||
|
|
||||||
{/* ── Stat cards ──────────────────────────────────────────── */}
|
{/* ── Bookings table card ──────────────────────────────────────── */}
|
||||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
|
<Card p={0} style={{ overflow: "hidden" }}>
|
||||||
<StatCard
|
{/* Toolbar */}
|
||||||
label="Total Bookings"
|
<Group
|
||||||
value={bookings.length}
|
justify="flex-end"
|
||||||
icon={<Package className="h-6 w-6" />}
|
gap={8}
|
||||||
gradient="from-emerald-500 to-emerald-700 shadow-emerald-500/30"
|
px={20}
|
||||||
/>
|
py={14}
|
||||||
<StatCard
|
style={{ borderBottom: "1px solid var(--mantine-color-edr-border-0)" }}
|
||||||
label="Active Bookings"
|
>
|
||||||
value={activeCount}
|
<Button
|
||||||
icon={<Truck className="h-6 w-6" />}
|
variant="default"
|
||||||
gradient="from-sky-500 to-blue-600 shadow-sky-500/30"
|
size="sm"
|
||||||
/>
|
radius="md"
|
||||||
<StatCard
|
leftSection={<ArrowUpDown size={14} />}
|
||||||
label="Pending Approval"
|
>
|
||||||
value={pendingCount}
|
Sort
|
||||||
icon={<Clock className="h-6 w-6" />}
|
</Button>
|
||||||
gradient="from-amber-400 to-orange-500 shadow-amber-500/30"
|
<Button
|
||||||
/>
|
variant="default"
|
||||||
</SimpleGrid>
|
size="sm"
|
||||||
|
radius="md"
|
||||||
{/* ── Table ───────────────────────────────────────────────── */}
|
leftSection={<Filter size={14} />}
|
||||||
<Card radius="lg" withBorder p={0} className="overflow-hidden">
|
>
|
||||||
<Group justify="space-between" align="flex-start" p="lg" className="border-b border-[var(--mantine-color-gray-2)] bg-gray-50/60">
|
|
||||||
<Box>
|
|
||||||
<Title order={4}>Recent Requests</Title>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
A list of your recent freight bookings and their statuses.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Button variant="default" size="sm" radius="md" leftSection={<Filter size={15} />}>
|
|
||||||
Filter
|
Filter
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
{/* Empty state */}
|
||||||
{total === 0 && dataTableStatus === "success" ? (
|
{total === 0 && dataTableStatus === "success" ? (
|
||||||
<Stack align="center" gap={4} px="lg" py={64} className="text-center">
|
<Stack align="center" gap={4} px="lg" py={64} ta="center">
|
||||||
<ThemeIcon size={56} radius="lg" color="edr-green" variant="light" mb="xs">
|
<ThemeIcon size={56} radius="lg" color="edr-green" variant="light" mb="xs">
|
||||||
<Package size={28} />
|
<Package size={28} />
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Text size="sm" fw={600}>
|
<Text size="sm" fw={600} c="edr-text">
|
||||||
No bookings found
|
No bookings yet
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" c="dimmed" maw={320}>
|
<Text size="xs" c="edr-muted" maw={320}>
|
||||||
{searchTerm
|
You haven't made any booking requests yet. Create your first one to get started.
|
||||||
? "No bookings match your current search filter."
|
|
||||||
: "You haven't requested any bookings yet."}
|
|
||||||
</Text>
|
</Text>
|
||||||
{!searchTerm && (
|
<Button
|
||||||
<Button
|
component={Link}
|
||||||
component={Link}
|
to="/bookings/new"
|
||||||
to="/bookings/new"
|
size="sm"
|
||||||
size="sm"
|
color="edr-green"
|
||||||
color="edr-green"
|
radius="md"
|
||||||
radius="md"
|
mt="md"
|
||||||
mt="md"
|
leftSection={<Plus size={15} />}
|
||||||
leftSection={<Plus size={15} />}
|
>
|
||||||
>
|
Create first booking
|
||||||
Create your first booking
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -295,14 +389,14 @@ export default function MyBookings() {
|
|||||||
pagination={{
|
pagination={{
|
||||||
pageIndex: pagination.pageIndex,
|
pageIndex: pagination.pageIndex,
|
||||||
pageSize: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
pageCount: pageCount,
|
pageCount,
|
||||||
totalCount: total,
|
totalCount: total,
|
||||||
}}
|
}}
|
||||||
tableOptions={{
|
tableOptions={{
|
||||||
state: { pagination },
|
state: { pagination },
|
||||||
onPaginationChange: setPagination,
|
onPaginationChange: setPagination,
|
||||||
}}
|
}}
|
||||||
containerClassName="border-0 shadow-none"
|
containerClassName="border-0 shadow-none rounded-none"
|
||||||
footer={DataTableFooter}
|
footer={DataTableFooter}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -311,60 +405,3 @@ export default function MyBookings() {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatCard({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
icon,
|
|
||||||
gradient,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: number;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
gradient: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
radius="lg"
|
|
||||||
withBorder
|
|
||||||
className="group transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md"
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center" wrap="nowrap">
|
|
||||||
<Box>
|
|
||||||
<Text size="sm" fw={500} c="dimmed">
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
<Text fz={30} fw={700} mt={6} className="tracking-tight">
|
|
||||||
{value}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
className={`flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br text-white shadow-md transition-transform duration-200 group-hover:scale-105 ${gradient}`}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</Box>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusBadge({ status }: { status: string }) {
|
|
||||||
const colorMap: Record<string, string> = {
|
|
||||||
DRAFT: "amber",
|
|
||||||
CONFIRMED: "edr-green",
|
|
||||||
IN_TRANSIT: "blue",
|
|
||||||
DELIVERED: "edr-green",
|
|
||||||
CANCELLED: "red",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
variant="light"
|
|
||||||
color={colorMap[status] ?? "gray"}
|
|
||||||
radius="sm"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{status.replace(/_/g, " ")}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user