mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +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 { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Menu,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
ArrowRight,
|
||||
Clock,
|
||||
Eye,
|
||||
Filter,
|
||||
MoreHorizontal,
|
||||
Package,
|
||||
Plus,
|
||||
Search,
|
||||
Truck,
|
||||
} from "lucide-react";
|
||||
import { ArrowUpDown, Download, Filter, MoreVertical, Package, Plus } from "lucide-react";
|
||||
|
||||
import { api } from "@/services/api";
|
||||
import type { Freight } from "@edr/types";
|
||||
@@ -37,63 +24,188 @@ import {
|
||||
usePagination,
|
||||
} 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() {
|
||||
const navigate = useNavigate();
|
||||
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 filteredData = useMemo(() => {
|
||||
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 total = bookings.length;
|
||||
const pageCount = Math.ceil(total / pagination.pageSize);
|
||||
const start = pagination.pageIndex * pagination.pageSize;
|
||||
const end = Math.min(start + pagination.pageSize, total);
|
||||
|
||||
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 paginatedData = useMemo(() => bookings.slice(start, end), [bookings, start, end]);
|
||||
|
||||
const columns: ColumnDef<Freight.IBooking>[] = [
|
||||
{
|
||||
accessorKey: "reference",
|
||||
header: "Reference",
|
||||
id: "booking",
|
||||
size: 244,
|
||||
meta: hMeta,
|
||||
header: () => <ColHeader label="Booking" />,
|
||||
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 (
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<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">
|
||||
<Package className="h-5 w-5" />
|
||||
<Group gap={12} wrap="nowrap" align="center">
|
||||
<Box
|
||||
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>
|
||||
<Text fw={600} size="sm">
|
||||
{booking.reference}
|
||||
<Box style={{ minWidth: 0 }}>
|
||||
<Text fz={14} fw={700} c="edr-text" truncate>
|
||||
{b.reference}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{booking.scheduledDate ?? booking.createdAt}
|
||||
<Text fz={12} c="edr-muted">
|
||||
{cargoLabel}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
@@ -102,72 +214,80 @@ export default function MyBookings() {
|
||||
},
|
||||
{
|
||||
id: "route",
|
||||
header: "Route",
|
||||
cell: ({ row }) => (
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<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",
|
||||
size: 196,
|
||||
meta: hMeta,
|
||||
header: () => <ColHeader label="Route" />,
|
||||
cell: ({ row }) => {
|
||||
const b = row.original;
|
||||
const containerCount = b.containers?.reduce((sum, c) => sum + c.qty, 0) ?? 0;
|
||||
const containerType = b.containers?.[0]?.type ?? null;
|
||||
const origin = b.originYard?.label ?? b.originYard?.code ?? "—";
|
||||
const dest = b.destinationYard?.label ?? b.destinationYard?.code ?? "—";
|
||||
const sub = b.scheduledDate ?? b.createdAt ?? "";
|
||||
return (
|
||||
<Box>
|
||||
<Text size="sm" fw={500}>
|
||||
{b.freightType === "BULK" ? "Bulk" : "Break Bulk"}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{containerType && containerCount > 0 ? `${containerCount} × ${containerType} · ` : ""}
|
||||
{b.cargoTotalWeightVgm}t
|
||||
<Text fz={13} fw={600} c="edr-text">
|
||||
{origin} → {dest}
|
||||
</Text>
|
||||
{sub && (
|
||||
<Text fz={12} c="edr-muted">
|
||||
{sub}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "transportMode",
|
||||
header: "Transport",
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm" c="dimmed">
|
||||
{row.original.serviceType === "RAIL_AND_FORWARDING" ? "Rail & Forwarding" : "Rail"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
id: "status",
|
||||
size: 190,
|
||||
meta: hMeta,
|
||||
header: () => <ColHeader label="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",
|
||||
size: 40,
|
||||
meta: hMeta,
|
||||
header: () => null,
|
||||
cell: ({ row }) => {
|
||||
const booking = row.original;
|
||||
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.Target>
|
||||
<ActionIcon variant="default" radius="md" aria-label="Row actions">
|
||||
<MoreHorizontal size={16} />
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={30}
|
||||
radius="md"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVertical size={16} color="#9AA8B5" />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
leftSection={<Eye size={15} />}
|
||||
onClick={() => navigate(`/bookings/${booking.id}`)}
|
||||
>
|
||||
View
|
||||
<Menu.Item onClick={() => navigate(`/bookings/${booking.id}`)}>
|
||||
View Details
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
@@ -180,111 +300,85 @@ export default function MyBookings() {
|
||||
const dataTableStatus = isLoading ? "loading" : isError ? "error" : "success";
|
||||
|
||||
return (
|
||||
<Box className="min-h-screen bg-gray-50/50 p-6">
|
||||
<Stack gap="lg" maw={1280} mx="auto">
|
||||
{/* ── Header band ─────────────────────────────────────────── */}
|
||||
<Card
|
||||
radius="lg"
|
||||
withBorder
|
||||
className="relative overflow-hidden border-emerald-100! bg-gradient-to-br from-emerald-50 via-white to-white"
|
||||
>
|
||||
<Box className="pointer-events-none absolute -right-10 -top-16 h-48 w-48 rounded-full bg-emerald-400/10 blur-2xl" />
|
||||
<Group justify="space-between" align="flex-end" wrap="wrap" gap="md" className="relative">
|
||||
<Box>
|
||||
<Title order={1} className="tracking-tight">
|
||||
My Bookings
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" mt={4}>
|
||||
View and manage your freight booking requests.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Group gap="sm" wrap="wrap">
|
||||
<TextInput
|
||||
placeholder="Search bookings..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.currentTarget.value)}
|
||||
leftSection={<Search size={16} />}
|
||||
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>
|
||||
<Box style={{ padding: "28px 32px 32px" }}>
|
||||
<Stack gap="lg">
|
||||
{/* ── Page header ─────────────────────────────────────────────── */}
|
||||
<Group justify="space-between" align="flex-end" wrap="wrap" gap="md">
|
||||
<Box>
|
||||
<Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
|
||||
Bookings
|
||||
</Title>
|
||||
<Text size="sm" c="edr-muted" mt={4}>
|
||||
Manage every cargo booking — from draft to delivery.
|
||||
</Text>
|
||||
</Box>
|
||||
<Group gap={12}>
|
||||
<Button variant="default" radius="md" leftSection={<Download size={16} />}>
|
||||
Export
|
||||
</Button>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/bookings/new"
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
leftSection={<Plus size={16} />}
|
||||
>
|
||||
New Booking
|
||||
</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
</Group>
|
||||
|
||||
{/* ── Stat cards ──────────────────────────────────────────── */}
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
|
||||
<StatCard
|
||||
label="Total Bookings"
|
||||
value={bookings.length}
|
||||
icon={<Package className="h-6 w-6" />}
|
||||
gradient="from-emerald-500 to-emerald-700 shadow-emerald-500/30"
|
||||
/>
|
||||
<StatCard
|
||||
label="Active Bookings"
|
||||
value={activeCount}
|
||||
icon={<Truck className="h-6 w-6" />}
|
||||
gradient="from-sky-500 to-blue-600 shadow-sky-500/30"
|
||||
/>
|
||||
<StatCard
|
||||
label="Pending Approval"
|
||||
value={pendingCount}
|
||||
icon={<Clock className="h-6 w-6" />}
|
||||
gradient="from-amber-400 to-orange-500 shadow-amber-500/30"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* ── Table ───────────────────────────────────────────────── */}
|
||||
<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} />}>
|
||||
{/* ── Bookings table card ──────────────────────────────────────── */}
|
||||
<Card p={0} style={{ overflow: "hidden" }}>
|
||||
{/* Toolbar */}
|
||||
<Group
|
||||
justify="flex-end"
|
||||
gap={8}
|
||||
px={20}
|
||||
py={14}
|
||||
style={{ borderBottom: "1px solid var(--mantine-color-edr-border-0)" }}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
radius="md"
|
||||
leftSection={<ArrowUpDown size={14} />}
|
||||
>
|
||||
Sort
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
radius="md"
|
||||
leftSection={<Filter size={14} />}
|
||||
>
|
||||
Filter
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Empty state */}
|
||||
{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">
|
||||
<Package size={28} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" fw={600}>
|
||||
No bookings found
|
||||
<Text size="sm" fw={600} c="edr-text">
|
||||
No bookings yet
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" maw={320}>
|
||||
{searchTerm
|
||||
? "No bookings match your current search filter."
|
||||
: "You haven't requested any bookings yet."}
|
||||
<Text size="xs" c="edr-muted" maw={320}>
|
||||
You haven't made any booking requests yet. Create your first one to get started.
|
||||
</Text>
|
||||
{!searchTerm && (
|
||||
<Button
|
||||
component={Link}
|
||||
to="/bookings/new"
|
||||
size="sm"
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
mt="md"
|
||||
leftSection={<Plus size={15} />}
|
||||
>
|
||||
Create your first booking
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
component={Link}
|
||||
to="/bookings/new"
|
||||
size="sm"
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
mt="md"
|
||||
leftSection={<Plus size={15} />}
|
||||
>
|
||||
Create first booking
|
||||
</Button>
|
||||
</Stack>
|
||||
) : (
|
||||
<DataTable
|
||||
@@ -295,14 +389,14 @@ export default function MyBookings() {
|
||||
pagination={{
|
||||
pageIndex: pagination.pageIndex,
|
||||
pageSize: pagination.pageSize,
|
||||
pageCount: pageCount,
|
||||
pageCount,
|
||||
totalCount: total,
|
||||
}}
|
||||
tableOptions={{
|
||||
state: { pagination },
|
||||
onPaginationChange: setPagination,
|
||||
}}
|
||||
containerClassName="border-0 shadow-none"
|
||||
containerClassName="border-0 shadow-none rounded-none"
|
||||
footer={DataTableFooter}
|
||||
/>
|
||||
)}
|
||||
@@ -311,60 +405,3 @@ export default function MyBookings() {
|
||||
</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