mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
fixes
This commit is contained in:
@@ -1,4 +1,12 @@
|
|||||||
import { Box, Grid, Group, SimpleGrid, Skeleton, Stack, Text } from "@mantine/core";
|
import {
|
||||||
|
Box,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
SimpleGrid,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
} from "@mantine/core";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import {
|
import {
|
||||||
@@ -14,7 +22,7 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import useAuth from "@/hooks/useAuth";
|
import useAuth from "@/hooks/useAuth";
|
||||||
@@ -30,7 +38,12 @@ const cv = (token: string) => {
|
|||||||
return `var(--mantine-color-${name}-${shade ?? "6"})`;
|
return `var(--mantine-color-${name}-${shade ?? "6"})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ACTIVE_STATUSES = ["DRAFT", "SUBMITTED", "PENDING_APPROVAL", "IN_TRANSIT"];
|
const ACTIVE_STATUSES = [
|
||||||
|
"DRAFT",
|
||||||
|
"SUBMITTED",
|
||||||
|
"PENDING_APPROVAL",
|
||||||
|
"IN_TRANSIT",
|
||||||
|
];
|
||||||
|
|
||||||
interface StageConfig {
|
interface StageConfig {
|
||||||
stage: number;
|
stage: number;
|
||||||
@@ -43,7 +56,11 @@ interface StageConfig {
|
|||||||
badgeBg: string;
|
badgeBg: string;
|
||||||
badgeText: string;
|
badgeText: string;
|
||||||
badgeDot: string;
|
badgeDot: string;
|
||||||
action: { label: string; kind: "dark" | "amber" | "outline"; icon?: LucideIcon };
|
action: {
|
||||||
|
label: string;
|
||||||
|
kind: "dark" | "amber" | "outline";
|
||||||
|
icon?: LucideIcon;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_CONFIG: Record<string, StageConfig> = {
|
const STATUS_CONFIG: Record<string, StageConfig> = {
|
||||||
@@ -146,7 +163,10 @@ const ACTION_PROPS: Record<string, { bg: string; c: string; bd?: string }> = {
|
|||||||
outline: { bg: "edr-card", c: "edr-text", bd: "1px solid edr-border" },
|
outline: { bg: "edr-card", c: "edr-text", bd: "1px solid edr-border" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const INVOICE_BADGE: Record<InvoiceStatus, { label: string; bg: string; text: string }> = {
|
const INVOICE_BADGE: Record<
|
||||||
|
InvoiceStatus,
|
||||||
|
{ label: string; bg: string; text: string }
|
||||||
|
> = {
|
||||||
Draft: { label: "Draft", bg: "edr-slate-soft", text: "edr-slate" },
|
Draft: { label: "Draft", bg: "edr-slate-soft", text: "edr-slate" },
|
||||||
Sent: { label: "Due soon", bg: "edr-amber-soft", text: "edr-amber-text" },
|
Sent: { label: "Due soon", bg: "edr-amber-soft", text: "edr-amber-text" },
|
||||||
Paid: { label: "Paid", bg: "edr-soft", text: "edr-green.7" },
|
Paid: { label: "Paid", bg: "edr-soft", text: "edr-green.7" },
|
||||||
@@ -157,108 +177,154 @@ const INVOICE_BADGE: Record<InvoiceStatus, { label: string; bg: string; text: st
|
|||||||
const MONTHS = ["Dec", "Jan", "Feb", "Mar", "Apr", "May"];
|
const MONTHS = ["Dec", "Jan", "Feb", "Mar", "Apr", "May"];
|
||||||
const VOLUME_DATA = [420, 680, 510, 820, 750, 940];
|
const VOLUME_DATA = [420, 680, 510, 820, 750, 940];
|
||||||
|
|
||||||
type Tab = "all" | "needs" | "completed";
|
|
||||||
const NEEDS_ACTION = ["DRAFT", "PENDING_APPROVAL"];
|
|
||||||
|
|
||||||
export default function MyPortalPage() {
|
export default function MyPortalPage() {
|
||||||
const { user, customer } = useAuth();
|
const { user, customer } = useAuth();
|
||||||
const myShipments = useMemo(() => getMyShipments(), []);
|
const myShipments = useMemo(() => getMyShipments(), []);
|
||||||
const myInvoices = useMemo(() => getMyInvoices(), []);
|
const myInvoices = useMemo(() => getMyInvoices(), []);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [tab, setTab] = useState<Tab>("all");
|
|
||||||
|
|
||||||
const bookingsQuery = useQuery(
|
const bookingsQuery = useQuery(
|
||||||
api.bookings.list.queryOptions({ input: { sortBy: "createdAt", sortOrder: "DESC" } }),
|
api.bookings.list.queryOptions({
|
||||||
|
input: { sortBy: "createdAt", sortOrder: "DESC" },
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const allBookings = bookingsQuery.data?.items ?? [];
|
const allBookings = bookingsQuery.data?.items ?? [];
|
||||||
const activeBookings = allBookings.filter((b) => ACTIVE_STATUSES.includes(b.status));
|
const activeBookings = allBookings.filter((b) =>
|
||||||
|
ACTIVE_STATUSES.includes(b.status),
|
||||||
|
);
|
||||||
|
|
||||||
const visibleBookings = allBookings
|
const visibleBookings = allBookings;
|
||||||
const outstandingInvoices = myInvoices.filter(
|
const outstandingInvoices = myInvoices.filter(
|
||||||
(inv) => inv.status === "Sent" || inv.status === "Overdue",
|
(inv) => inv.status === "Sent" || inv.status === "Overdue",
|
||||||
);
|
);
|
||||||
const totalOutstanding = outstandingInvoices.reduce((sum, inv) => sum + inv.amount, 0);
|
const totalOutstanding = outstandingInvoices.reduce(
|
||||||
const deliveredCount = myShipments.filter((s) => s.status === "Delivered").length || 12;
|
(sum, inv) => sum + inv.amount,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const deliveredCount =
|
||||||
|
myShipments.filter((s) => s.status === "Delivered").length || 12;
|
||||||
|
|
||||||
const displayName = user?.name?.en ?? user?.username ?? user?.email ?? "—";
|
const displayName = user?.name?.en ?? user?.username ?? user?.email ?? "—";
|
||||||
const companyName = (customer as any)?.companyName ?? displayName;
|
const companyName = (customer as any)?.companyName ?? displayName;
|
||||||
|
|
||||||
const hour = new Date().getHours();
|
const hour = new Date().getHours();
|
||||||
const greeting = hour < 12 ? "Good morning," : hour < 18 ? "Good afternoon," : "Good evening,";
|
const greeting =
|
||||||
|
hour < 12
|
||||||
|
? "Good morning,"
|
||||||
|
: hour < 18
|
||||||
|
? "Good afternoon,"
|
||||||
|
: "Good evening,";
|
||||||
const recentInvoices = myInvoices.slice(0, 3);
|
const recentInvoices = myInvoices.slice(0, 3);
|
||||||
const maxVolume = Math.max(...VOLUME_DATA);
|
const maxVolume = Math.max(...VOLUME_DATA);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg" p={{ base: 16, sm: 24, lg: 32 }}>
|
<Stack gap="lg" p={{ base: 16, sm: 24, lg: 32 }}>
|
||||||
{/* ── Hello Row ─────────────────────────────────────────────────────── */}
|
{/* ── Hello Row ─────────────────────────────────────────────────────── */}
|
||||||
<Group
|
<Group justify="space-between" align="center" gap="md">
|
||||||
justify="space-between"
|
|
||||||
align="center"
|
|
||||||
gap="md"
|
|
||||||
className="!flex-col !items-stretch md:!flex-row md:!items-center"
|
|
||||||
>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="sm" c="edr-muted">{greeting}</Text>
|
<Text size="sm" c="edr-muted">
|
||||||
|
{greeting}
|
||||||
|
</Text>
|
||||||
<Text fz={26} fw={800} mt={2} c="edr-text" className="tracking-tight">
|
<Text fz={26} fw={800} mt={2} c="edr-text" className="tracking-tight">
|
||||||
{companyName} 👋
|
{companyName} 👋
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Book a shipment CTA */}
|
{/* Book a shipment CTA */}
|
||||||
<Group
|
<Link to="/bookings/new">
|
||||||
component={Link as any}
|
<Group
|
||||||
to="/bookings/new"
|
gap={14}
|
||||||
gap={14}
|
align="center"
|
||||||
align="center"
|
wrap="nowrap"
|
||||||
wrap="nowrap"
|
bg="edr-green"
|
||||||
bg="edr-green"
|
px={18}
|
||||||
px={18}
|
py={14}
|
||||||
py={14}
|
className="w-full md:w-60! rounded-2xl no-underline shadow-[0_6px_10px_-12px_rgba(14,163,83,0.8)]"
|
||||||
className="w-full md:!w-[240px] rounded-2xl no-underline shadow-[0_6px_10px_-12px_rgba(14,163,83,0.8)]"
|
>
|
||||||
>
|
|
||||||
|
|
||||||
<Truck size={22} color="#fff" />
|
<Truck size={22} color="#fff" />
|
||||||
|
|
||||||
<Box className="min-w-0 flex-1">
|
<Box className="min-w-0 flex-1">
|
||||||
<Text fz={14} fw={700} c="white" lh={1.3}>Book a shipment</Text>
|
<Text fz={14} fw={700} c="white" lh={1.3}>
|
||||||
</Box>
|
Book a shipment
|
||||||
<Box className="flex h-[32px] w-[32px] shrink-0 items-center justify-center rounded-full bg-white">
|
</Text>
|
||||||
<ArrowRight size={18} color={cv("edr-green.7")} />
|
</Box>
|
||||||
</Box>
|
<Box className="flex h-[32px] w-[32px] shrink-0 items-center justify-center rounded-full bg-white">
|
||||||
</Group>
|
<ArrowRight size={18} color={cv("edr-green.7")} />
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Link>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* ── Stats Strip ───────────────────────────────────────────────────── */}
|
{/* ── Stats Strip ───────────────────────────────────────────────────── */}
|
||||||
<Box className="rounded-2xl border border-edr-border bg-gradient-to-br from-white to-edr-soft px-7 py-5">
|
<Box className="rounded-2xl border border-edr-border bg-gradient-to-br from-white to-edr-soft px-7 py-5">
|
||||||
<SimpleGrid cols={{ base: 2, lg: 4 }} spacing={0} verticalSpacing={20}>
|
<SimpleGrid cols={{ base: 2, lg: 4 }} spacing={0} verticalSpacing={20}>
|
||||||
<StatKpi icon={Truck} label="Active Shipments" value={activeBookings.length.toString()} delta="+2 this week" deltaColor="edr-green.7" />
|
<StatKpi
|
||||||
<StatKpi icon={Clock3} label="Awaiting Payment" value={outstandingInvoices.length.toString()} delta={`${formatCurrency(totalOutstanding || 377500, "ETB")} due`} deltaColor="edr-amber-text" divider />
|
icon={Truck}
|
||||||
<StatKpi icon={CheckCircle2} label="Delivered (May)" value={deliveredCount.toString()} delta="96% on-time" deltaColor="edr-muted" divider />
|
label="Active Shipments"
|
||||||
<StatKpi icon={Wallet} label="Spend YTD" value="ETB 1.24M" delta="+16% YoY" deltaColor="edr-green.7" divider />
|
value={activeBookings.length.toString()}
|
||||||
|
delta="+2 this week"
|
||||||
|
deltaColor="edr-green.7"
|
||||||
|
/>
|
||||||
|
<StatKpi
|
||||||
|
icon={Clock3}
|
||||||
|
label="Awaiting Payment"
|
||||||
|
value={outstandingInvoices.length.toString()}
|
||||||
|
delta={`${formatCurrency(totalOutstanding || 377500, "ETB")} due`}
|
||||||
|
deltaColor="edr-amber-text"
|
||||||
|
divider
|
||||||
|
/>
|
||||||
|
<StatKpi
|
||||||
|
icon={CheckCircle2}
|
||||||
|
label="Delivered (May)"
|
||||||
|
value={deliveredCount.toString()}
|
||||||
|
delta="96% on-time"
|
||||||
|
deltaColor="edr-muted"
|
||||||
|
divider
|
||||||
|
/>
|
||||||
|
<StatKpi
|
||||||
|
icon={Wallet}
|
||||||
|
label="Spend YTD"
|
||||||
|
value="ETB 1.24M"
|
||||||
|
delta="+16% YoY"
|
||||||
|
deltaColor="edr-green.7"
|
||||||
|
divider
|
||||||
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* ── Mid Row: Shipments + Invoices ─────────────────────────────────── */}
|
{/* ── Mid Row: Shipments + Invoices ─────────────────────────────────── */}
|
||||||
<Grid align="stretch">
|
<Grid align="stretch">
|
||||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||||
<Card className="h-full" padding={28}>
|
<Card className="h-full" padding={28}>
|
||||||
<Group justify="space-between" align="center" mb={18} wrap="nowrap">
|
<Group justify="space-between" align="center" mb={18} wrap="nowrap">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={19} fw={800} c="edr-text">My Shipments</Text>
|
<Text fz={19} fw={800} c="edr-text">
|
||||||
<Text fz={13} c="edr-muted">From draft to delivery — every booking in one place</Text>
|
My Shipments
|
||||||
|
</Text>
|
||||||
|
<Text fz={13} c="edr-muted">
|
||||||
|
From draft to delivery — every booking in one place
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{bookingsQuery.isPending ? (
|
{bookingsQuery.isPending ? (
|
||||||
<Stack gap={6}>{[1, 2, 3, 4].map((i) => <Skeleton key={i} height={64} radius="md" />)}</Stack>
|
<Stack gap={6}>
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<Skeleton key={i} height={64} radius="md" />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
) : visibleBookings.length === 0 ? (
|
) : visibleBookings.length === 0 ? (
|
||||||
<EmptyState message="No bookings in this view." />
|
<EmptyState message="No bookings in this view." />
|
||||||
) : (
|
) : (
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
{visibleBookings.map((booking, i) => (
|
{visibleBookings.map((booking, i) => (
|
||||||
<BookingRow key={booking.id} booking={booking} last={i === visibleBookings.length - 1} onClick={() => navigate(`/bookings/${booking.id}`)} />
|
<BookingRow
|
||||||
|
key={booking.id}
|
||||||
|
booking={booking}
|
||||||
|
last={i === visibleBookings.length - 1}
|
||||||
|
onClick={() => navigate(`/bookings/${booking.id}`)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@@ -269,22 +335,48 @@ export default function MyPortalPage() {
|
|||||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
<Card className="h-full" padding={24}>
|
<Card className="h-full" padding={24}>
|
||||||
<Group justify="space-between" align="center" mb={16}>
|
<Group justify="space-between" align="center" mb={16}>
|
||||||
<Text fz={17} fw={700} c="edr-text">Invoices</Text>
|
<Text fz={17} fw={700} c="edr-text">
|
||||||
<Group component={Link as any} to="/billing" gap={3} align="center" className="no-underline">
|
Invoices
|
||||||
<Text fz={13} fw={600} c="edr-green.7">View all</Text>
|
</Text>
|
||||||
<ChevronRight size={15} color={cv("edr-green.7")} />
|
<Link to="/billing">
|
||||||
</Group>
|
<Group gap={3} align="center" className="no-underline">
|
||||||
|
<Text fz={13} fw={600} c="edr-green.7">
|
||||||
|
View all
|
||||||
|
</Text>
|
||||||
|
<ChevronRight size={15} color={cv("edr-green.7")} />
|
||||||
|
</Group>
|
||||||
|
</Link>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Outstanding card */}
|
{/* Outstanding card */}
|
||||||
<Box mb={16} p={16} bg="edr-amber-soft" className="rounded-[14px]">
|
<Box mb={16} p={16} bg="edr-amber-soft" className="rounded-[14px]">
|
||||||
<Text fz={12} fw={600} c="edr-amber-text">Outstanding balance</Text>
|
<Text fz={12} fw={600} c="edr-amber-text">
|
||||||
<Text fz={24} fw={800} mt={4} c="edr-text">{formatCurrency(totalOutstanding || 377500, "ETB")}</Text>
|
Outstanding balance
|
||||||
<Group justify="space-between" align="center" mt={8} wrap="nowrap">
|
</Text>
|
||||||
<Text fz={12} c="edr-amber-text">{outstandingInvoices.length || 2} invoices unpaid</Text>
|
<Text fz={24} fw={800} mt={4} c="edr-text">
|
||||||
<Group gap={5} align="center" px={14} py={8} bg="edr-accent" className="cursor-pointer rounded-[9px]">
|
{formatCurrency(totalOutstanding || 377500, "ETB")}
|
||||||
|
</Text>
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
mt={8}
|
||||||
|
wrap="nowrap"
|
||||||
|
>
|
||||||
|
<Text fz={12} c="edr-amber-text">
|
||||||
|
{outstandingInvoices.length || 2} invoices unpaid
|
||||||
|
</Text>
|
||||||
|
<Group
|
||||||
|
gap={5}
|
||||||
|
align="center"
|
||||||
|
px={14}
|
||||||
|
py={8}
|
||||||
|
bg="edr-accent"
|
||||||
|
className="cursor-pointer rounded-[9px]"
|
||||||
|
>
|
||||||
<Zap size={15} color="#fff" />
|
<Zap size={15} color="#fff" />
|
||||||
<Text fz={13} fw={700} c="white">Pay all</Text>
|
<Text fz={13} fw={700} c="white">
|
||||||
|
Pay all
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -302,26 +394,53 @@ export default function MyPortalPage() {
|
|||||||
: invoice.status === "Overdue"
|
: invoice.status === "Overdue"
|
||||||
? "Overdue 3 days"
|
? "Overdue 3 days"
|
||||||
: `Due ${invoice.dueDate}`;
|
: `Due ${invoice.dueDate}`;
|
||||||
const DueIcon = invoice.status === "Paid" ? CheckCircle2 : Clock3;
|
const DueIcon =
|
||||||
const dueIconColor = invoice.status === "Paid" ? cv("edr-green.5") : cv("edr-muted");
|
invoice.status === "Paid" ? CheckCircle2 : Clock3;
|
||||||
|
const dueIconColor =
|
||||||
|
invoice.status === "Paid"
|
||||||
|
? cv("edr-green.5")
|
||||||
|
: cv("edr-muted");
|
||||||
return (
|
return (
|
||||||
<Box key={invoice.id}>
|
<Box key={invoice.id}>
|
||||||
{i > 0 && <Box h={1} bg="edr-divider" />}
|
{i > 0 && <Box h={1} bg="edr-divider" />}
|
||||||
<Stack gap={8} py={10}>
|
<Stack gap={8} py={10}>
|
||||||
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
align="flex-start"
|
||||||
|
wrap="nowrap"
|
||||||
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={13} fw={700} c="edr-text">{invoice.number}</Text>
|
<Text fz={13} fw={700} c="edr-text">
|
||||||
<Text fz={11} c="edr-muted">{invoice.bookingReference}</Text>
|
{invoice.number}
|
||||||
|
</Text>
|
||||||
|
<Text fz={11} c="edr-muted">
|
||||||
|
{invoice.bookingReference}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Text fz={14} fw={700} c="edr-text">{formatCurrency(invoice.amount, invoice.currency)}</Text>
|
<Text fz={14} fw={700} c="edr-text">
|
||||||
|
{formatCurrency(invoice.amount, invoice.currency)}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group justify="space-between" align="center" wrap="nowrap">
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
wrap="nowrap"
|
||||||
|
>
|
||||||
<Group gap={5} align="center">
|
<Group gap={5} align="center">
|
||||||
<DueIcon size={13} color={dueIconColor} />
|
<DueIcon size={13} color={dueIconColor} />
|
||||||
<Text fz={12} c="edr-muted">{dueText}</Text>
|
<Text fz={12} c="edr-muted">
|
||||||
|
{dueText}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Box bg={badge.bg} px={10} py={4} className="rounded-full">
|
<Box
|
||||||
<Text fz={11} fw={700} c={badge.text}>{badge.label}</Text>
|
bg={badge.bg}
|
||||||
|
px={10}
|
||||||
|
py={4}
|
||||||
|
className="rounded-full"
|
||||||
|
>
|
||||||
|
<Text fz={11} fw={700} c={badge.text}>
|
||||||
|
{badge.label}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -335,27 +454,40 @@ export default function MyPortalPage() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* ── Bottom Row: Freight Volume + Recent Activity ──────────────────── */}
|
{/* ── Bottom Row: Freight Volume + Recent Activity ──────────────────── */}
|
||||||
<Grid gutter={20} align="stretch">
|
<Grid align="stretch">
|
||||||
<Grid.Col span={{ base: 12, md: 5 }}>
|
<Grid.Col span={{ base: 12, md: 5 }}>
|
||||||
<Card className="h-full" padding={24}>
|
<Card className="h-full" padding={24}>
|
||||||
<Text fz={17} fw={700} c="edr-text">Freight Volume</Text>
|
<Text fz={17} fw={700} c="edr-text">
|
||||||
|
Freight Volume
|
||||||
|
</Text>
|
||||||
<Group gap={10} align="baseline" mt={4} mb={22}>
|
<Group gap={10} align="baseline" mt={4} mb={22}>
|
||||||
<Text fz={26} fw={800} c="edr-text">4,180 t</Text>
|
<Text fz={26} fw={800} c="edr-text">
|
||||||
<Text fz={13} c="edr-muted">ETB 1.24M</Text>
|
4,180 t
|
||||||
<Text fz={12} fw={700} c="edr-green.7">+16% YTD</Text>
|
</Text>
|
||||||
|
<Text fz={13} c="edr-muted">
|
||||||
|
ETB 1.24M
|
||||||
|
</Text>
|
||||||
|
<Text fz={12} fw={700} c="edr-green.7">
|
||||||
|
+16% YTD
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group align="flex-end" gap={10} className="h-[110px]">
|
<Group align="flex-end" gap={10} className="h-[110px]">
|
||||||
{VOLUME_DATA.map((val, i) => {
|
{VOLUME_DATA.map((val, i) => {
|
||||||
const isLast = i === VOLUME_DATA.length - 1;
|
const isLast = i === VOLUME_DATA.length - 1;
|
||||||
return (
|
return (
|
||||||
<Box key={i} className="flex flex-1 flex-col items-center gap-2">
|
<Box
|
||||||
|
key={i}
|
||||||
|
className="flex flex-1 flex-col items-center gap-2"
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
bg={isLast ? "edr-green" : "edr-soft"}
|
bg={isLast ? "edr-green" : "edr-soft"}
|
||||||
bd={isLast ? undefined : "1px solid edr-border"}
|
bd={isLast ? undefined : "1px solid edr-border"}
|
||||||
h={Math.round((val / maxVolume) * 86)}
|
h={Math.round((val / maxVolume) * 86)}
|
||||||
className="w-full rounded-t-md"
|
className="w-full rounded-t-md"
|
||||||
/>
|
/>
|
||||||
<Text fz={11} c="edr-muted">{MONTHS[i]}</Text>
|
<Text fz={11} c="edr-muted">
|
||||||
|
{MONTHS[i]}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -366,21 +498,35 @@ export default function MyPortalPage() {
|
|||||||
<Grid.Col span={{ base: 12, md: 7 }}>
|
<Grid.Col span={{ base: 12, md: 7 }}>
|
||||||
<Card className="h-full" padding={24}>
|
<Card className="h-full" padding={24}>
|
||||||
<Group justify="space-between" align="center" mb={16}>
|
<Group justify="space-between" align="center" mb={16}>
|
||||||
<Text fz={17} fw={700} c="edr-text">Recent Activity</Text>
|
<Text fz={17} fw={700} c="edr-text">
|
||||||
<Group component={Link as any} to="/bookings" gap={3} align="center" className="no-underline">
|
Recent Activity
|
||||||
<Text fz={13} fw={600} c="edr-green.7">View all</Text>
|
</Text>
|
||||||
<ChevronRight size={15} color={cv("edr-green.7")} />
|
<Link to="/bookings">
|
||||||
</Group>
|
<Group gap={3} align="center" className="no-underline">
|
||||||
|
<Text fz={13} fw={600} c="edr-green.7">
|
||||||
|
View all
|
||||||
|
</Text>
|
||||||
|
<ChevronRight size={15} color={cv("edr-green.7")} />
|
||||||
|
</Group>
|
||||||
|
</Link>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{bookingsQuery.isPending ? (
|
{bookingsQuery.isPending ? (
|
||||||
<Stack gap={10}>{[1, 2, 3, 4, 5].map((i) => <Skeleton key={i} height={44} radius="md" />)}</Stack>
|
<Stack gap={10}>
|
||||||
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
|
<Skeleton key={i} height={44} radius="md" />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
) : allBookings.length === 0 ? (
|
) : allBookings.length === 0 ? (
|
||||||
<EmptyState message="No recent activity." />
|
<EmptyState message="No recent activity." />
|
||||||
) : (
|
) : (
|
||||||
<Stack gap={2}>
|
<Stack gap={2}>
|
||||||
{allBookings.slice(0, 6).map((booking) => (
|
{allBookings.slice(0, 6).map((booking) => (
|
||||||
<ActivityRow key={booking.id} booking={booking} onClick={() => navigate(`/bookings/${booking.id}`)} />
|
<ActivityRow
|
||||||
|
key={booking.id}
|
||||||
|
booking={booking}
|
||||||
|
onClick={() => navigate(`/bookings/${booking.id}`)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@@ -403,7 +549,10 @@ function Card({
|
|||||||
padding?: number;
|
padding?: number;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Box p={padding} className={`rounded-[20px] border border-edr-border bg-edr-card ${className}`}>
|
<Box
|
||||||
|
p={padding}
|
||||||
|
className={`rounded-[20px] border border-edr-border bg-edr-card ${className}`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -425,14 +574,25 @@ function StatKpi({
|
|||||||
divider?: boolean;
|
divider?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Box px={4} className={divider ? "lg:border-l lg:border-edr-border lg:pl-7" : undefined}>
|
<Box
|
||||||
|
px={4}
|
||||||
|
className={
|
||||||
|
divider ? "lg:border-l lg:border-edr-border lg:pl-7" : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
<Group gap={6} align="center" mb={7} wrap="nowrap">
|
<Group gap={6} align="center" mb={7} wrap="nowrap">
|
||||||
<Icon size={15} color={cv("edr-muted")} className="shrink-0" />
|
<Icon size={15} color={cv("edr-muted")} className="shrink-0" />
|
||||||
<Text fz={12} fw={600} c="edr-muted" truncate>{label}</Text>
|
<Text fz={12} fw={600} c="edr-muted" truncate>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap={8} align="flex-end" wrap="nowrap">
|
<Group gap={8} align="flex-end" wrap="nowrap">
|
||||||
<Text fz={22} fw={800} lh={1} c="edr-text" truncate>{value}</Text>
|
<Text fz={22} fw={800} lh={1} c="edr-text" truncate>
|
||||||
<Text fz={12} fw={600} lh={1.3} c={deltaColor} truncate>{delta}</Text>
|
{value}
|
||||||
|
</Text>
|
||||||
|
<Text fz={12} fw={600} lh={1.3} c={deltaColor} truncate>
|
||||||
|
{delta}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -446,9 +606,26 @@ function Stepper({ stage, color }: { stage: number; color: string }) {
|
|||||||
const active = i === stage;
|
const active = i === stage;
|
||||||
const size = active ? 12 : done ? 9 : 8;
|
const size = active ? 12 : done ? 9 : 8;
|
||||||
return (
|
return (
|
||||||
<Group key={i} gap={0} align="center" wrap="nowrap" className={i < 4 ? "flex-1" : undefined}>
|
<Group
|
||||||
<Box w={size} h={size} bg={done || active ? color : "edr-step-idle"} className="shrink-0 rounded-full" />
|
key={i}
|
||||||
{i < 4 && <Box h={3} bg={i < stage ? color : "edr-conn-idle"} className="flex-1 rounded-full" />}
|
gap={0}
|
||||||
|
align="center"
|
||||||
|
wrap="nowrap"
|
||||||
|
className={i < 4 ? "flex-1" : undefined}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
w={size}
|
||||||
|
h={size}
|
||||||
|
bg={done || active ? color : "edr-step-idle"}
|
||||||
|
className="shrink-0 rounded-full"
|
||||||
|
/>
|
||||||
|
{i < 4 && (
|
||||||
|
<Box
|
||||||
|
h={3}
|
||||||
|
bg={i < stage ? color : "edr-conn-idle"}
|
||||||
|
className="flex-1 rounded-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -456,43 +633,97 @@ function Stepper({ stage, color }: { stage: number; color: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BookingRow({ booking, last, onClick }: { booking: any; last: boolean; onClick: () => void }) {
|
function BookingRow({
|
||||||
|
booking,
|
||||||
|
last,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
booking: any;
|
||||||
|
last: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
const cfg = STATUS_CONFIG[booking.status] ?? STATUS_CONFIG.DRAFT;
|
const cfg = STATUS_CONFIG[booking.status] ?? STATUS_CONFIG.DRAFT;
|
||||||
const Icon = cfg.icon;
|
const Icon = cfg.icon;
|
||||||
const AIcon = cfg.action.icon;
|
const AIcon = cfg.action.icon;
|
||||||
const ap = ACTION_PROPS[cfg.action.kind];
|
const ap = ACTION_PROPS[cfg.action.kind];
|
||||||
const origin = booking.originYard?.label ?? booking.originYard?.code ?? "—";
|
const origin = booking.originYard?.label ?? booking.originYard?.code ?? "—";
|
||||||
const dest = booking.destinationYard?.label ?? booking.destinationYard?.code ?? "—";
|
const dest =
|
||||||
|
booking.destinationYard?.label ?? booking.destinationYard?.code ?? "—";
|
||||||
const commodity =
|
const commodity =
|
||||||
(typeof booking.cargoType === "string" ? booking.cargoType : booking.cargoType?.name) ??
|
(typeof booking.cargoType === "string"
|
||||||
|
? booking.cargoType
|
||||||
|
: booking.cargoType?.name) ??
|
||||||
booking.commodity ??
|
booking.commodity ??
|
||||||
"Freight";
|
"Freight";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={last ? undefined : "border-b border-edr-divider"}>
|
<Box className={last ? undefined : "border-b border-edr-divider"}>
|
||||||
<Group gap={16} align="center" wrap="nowrap" py={14} px={4} className="cursor-pointer" onClick={onClick}>
|
<Group
|
||||||
<Box w={46} h={46} bg={cfg.tile} className="flex shrink-0 items-center justify-center rounded-xl">
|
gap={16}
|
||||||
|
align="center"
|
||||||
|
wrap="nowrap"
|
||||||
|
py={14}
|
||||||
|
px={4}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
w={46}
|
||||||
|
h={46}
|
||||||
|
bg={cfg.tile}
|
||||||
|
className="flex shrink-0 items-center justify-center rounded-xl"
|
||||||
|
>
|
||||||
<Icon size={22} color={cv(cfg.iconColor)} />
|
<Icon size={22} color={cv(cfg.iconColor)} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className="min-w-0 flex-1 lg:!flex-none lg:!w-[188px]">
|
<Box className="min-w-0 flex-1 lg:!flex-none lg:!w-[188px]">
|
||||||
<Text fz={15} fw={700} c="edr-text" truncate>{booking.reference}</Text>
|
<Text fz={15} fw={700} c="edr-text" truncate>
|
||||||
<Text fz={12} c="edr-muted" truncate>{commodity} · {origin} → {dest}</Text>
|
{booking.reference}
|
||||||
|
</Text>
|
||||||
|
<Text fz={12} c="edr-muted" truncate>
|
||||||
|
{commodity} · {origin} → {dest}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className="hidden min-w-0 flex-1 pr-2 lg:block">
|
<Box className="hidden min-w-0 flex-1 pr-2 lg:block">
|
||||||
<Text fz={12} fw={500} mb={8} c={cfg.iconColor} truncate>{cfg.hint}</Text>
|
<Text fz={12} fw={500} mb={8} c={cfg.iconColor} truncate>
|
||||||
|
{cfg.hint}
|
||||||
|
</Text>
|
||||||
<Stepper stage={cfg.stage} color={cfg.step} />
|
<Stepper stage={cfg.stage} color={cfg.step} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack gap={9} align="flex-end" className="shrink-0">
|
<Stack gap={9} align="flex-end" className="shrink-0">
|
||||||
<Group gap={6} align="center" px={11} py={5} bg={cfg.badgeBg} className="rounded-full">
|
<Group
|
||||||
|
gap={6}
|
||||||
|
align="center"
|
||||||
|
px={11}
|
||||||
|
py={5}
|
||||||
|
bg={cfg.badgeBg}
|
||||||
|
className="rounded-full"
|
||||||
|
>
|
||||||
<Box w={6} h={6} bg={cfg.badgeDot} className="rounded-full" />
|
<Box w={6} h={6} bg={cfg.badgeDot} className="rounded-full" />
|
||||||
<Text fz={11} fw={700} c={cfg.badgeText}>{cfg.badgeLabel}</Text>
|
<Text fz={11} fw={700} c={cfg.badgeText}>
|
||||||
|
{cfg.badgeLabel}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap={5} align="center" px={15} py={8} bg={ap.bg} bd={ap.bd} className="cursor-pointer rounded-[9px]">
|
<Group
|
||||||
<Text fz={13} fw={700} c={ap.c}>{cfg.action.label}</Text>
|
gap={5}
|
||||||
{AIcon && <AIcon size={15} color={ap.c === "white" ? "#fff" : cv("edr-text")} />}
|
align="center"
|
||||||
|
px={15}
|
||||||
|
py={8}
|
||||||
|
bg={ap.bg}
|
||||||
|
bd={ap.bd}
|
||||||
|
className="cursor-pointer rounded-[9px]"
|
||||||
|
>
|
||||||
|
<Text fz={13} fw={700} c={ap.c}>
|
||||||
|
{cfg.action.label}
|
||||||
|
</Text>
|
||||||
|
{AIcon && (
|
||||||
|
<AIcon
|
||||||
|
size={15}
|
||||||
|
color={ap.c === "white" ? "#fff" : cv("edr-text")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -500,7 +731,13 @@ function BookingRow({ booking, last, onClick }: { booking: any; last: boolean; o
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActivityRow({ booking, onClick }: { booking: any; onClick: () => void }) {
|
function ActivityRow({
|
||||||
|
booking,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
booking: any;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
const cfg = STATUS_CONFIG[booking.status] ?? STATUS_CONFIG.DRAFT;
|
const cfg = STATUS_CONFIG[booking.status] ?? STATUS_CONFIG.DRAFT;
|
||||||
const Icon = cfg.icon;
|
const Icon = cfg.icon;
|
||||||
const verb =
|
const verb =
|
||||||
@@ -514,26 +751,49 @@ function ActivityRow({ booking, onClick }: { booking: any; onClick: () => void }
|
|||||||
? "submitted for review"
|
? "submitted for review"
|
||||||
: "created";
|
: "created";
|
||||||
return (
|
return (
|
||||||
<Group gap={12} align="center" wrap="nowrap" py={9} className="cursor-pointer" onClick={onClick}>
|
<Group
|
||||||
<Box w={36} h={36} bg={cfg.tile} className="flex shrink-0 items-center justify-center rounded-[10px]">
|
gap={12}
|
||||||
|
align="center"
|
||||||
|
wrap="nowrap"
|
||||||
|
py={9}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
w={36}
|
||||||
|
h={36}
|
||||||
|
bg={cfg.tile}
|
||||||
|
className="flex shrink-0 items-center justify-center rounded-[10px]"
|
||||||
|
>
|
||||||
<Icon size={17} color={cv(cfg.iconColor)} />
|
<Icon size={17} color={cv(cfg.iconColor)} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="min-w-0 flex-1">
|
<Box className="min-w-0 flex-1">
|
||||||
<Text fz={13} fw={600} c="edr-text" truncate>Booking {booking.reference} {verb}</Text>
|
<Text fz={13} fw={600} c="edr-text" truncate>
|
||||||
|
Booking {booking.reference} {verb}
|
||||||
|
</Text>
|
||||||
<Text fz={11} c="edr-muted" truncate>
|
<Text fz={11} c="edr-muted" truncate>
|
||||||
{booking.originYard?.label ?? booking.originYard?.code ?? "—"} →{" "}
|
{booking.originYard?.label ?? booking.originYard?.code ?? "—"} →{" "}
|
||||||
{booking.destinationYard?.label ?? booking.destinationYard?.code ?? "—"}
|
{booking.destinationYard?.label ??
|
||||||
|
booking.destinationYard?.code ??
|
||||||
|
"—"}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Text fz={11} c="edr-muted" className="shrink-0">{format(new Date(booking.createdAt), "MMM d")}</Text>
|
<Text fz={11} c="edr-muted" className="shrink-0">
|
||||||
|
{format(new Date(booking.createdAt), "MMM d")}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyState({ message }: { message: string }) {
|
function EmptyState({ message }: { message: string }) {
|
||||||
return (
|
return (
|
||||||
<Box py="xl" className="rounded-xl border border-dashed border-edr-border text-center">
|
<Box
|
||||||
<Text size="sm" c="edr-muted">{message}</Text>
|
py="xl"
|
||||||
|
className="rounded-xl border border-dashed border-edr-border text-center"
|
||||||
|
>
|
||||||
|
<Text size="sm" c="edr-muted">
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { Box, Group, SimpleGrid, Stack, Text, ThemeIcon, UnstyledButton } from "@mantine/core";
|
import {
|
||||||
|
Box,
|
||||||
|
Group,
|
||||||
|
SimpleGrid,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
UnstyledButton,
|
||||||
|
} from "@mantine/core";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
ArrowDownToLine,
|
ArrowDownToLine,
|
||||||
ArrowUpFromLine,
|
ArrowUpFromLine,
|
||||||
Building2,
|
Building2,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Ship,
|
|
||||||
Truck,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@@ -27,37 +33,37 @@ const USER_TYPE_CARDS: {
|
|||||||
description: string;
|
description: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
id: "importer",
|
id: "importer",
|
||||||
label: "Importer",
|
label: "Importer",
|
||||||
description: "Import goods into Ethiopia via the railway corridor.",
|
description: "Import goods into Ethiopia via the railway corridor.",
|
||||||
icon: <ArrowDownToLine size={22} />,
|
icon: <ArrowDownToLine size={22} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "exporter",
|
id: "exporter",
|
||||||
label: "Exporter",
|
label: "Exporter",
|
||||||
description: "Export goods from Ethiopia via rail.",
|
description: "Export goods from Ethiopia via rail.",
|
||||||
icon: <ArrowUpFromLine size={22} />,
|
icon: <ArrowUpFromLine size={22} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "freight-forwarder-et",
|
id: "freight-forwarder-et",
|
||||||
label: "Freight Forwarder (Ethiopia)",
|
label: "Freight Forwarder (Ethiopia)",
|
||||||
description: "Ethiopian freight forwarding company handling client cargo.",
|
description: "Ethiopian freight forwarding company handling client cargo.",
|
||||||
icon: <Building2 size={22} />,
|
icon: <Building2 size={22} />,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
id: "freight-forwarder-dj",
|
// id: "freight-forwarder-dj",
|
||||||
label: "FF Agent (Djibouti)",
|
// label: "FF Agent (Djibouti)",
|
||||||
description: "Djibouti-based agent coordinating cross-border logistics.",
|
// description: "Djibouti-based agent coordinating cross-border logistics.",
|
||||||
icon: <Ship size={22} />,
|
// icon: <Ship size={22} />,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "transporter",
|
// id: "transporter",
|
||||||
label: "Transporter",
|
// label: "Transporter",
|
||||||
description: "Trucking company providing first/last-mile services.",
|
// description: "Trucking company providing first/last-mile services.",
|
||||||
icon: <Truck size={22} />,
|
// icon: <Truck size={22} />,
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
const USER_TYPE_LEFT_MAP: Record<
|
const USER_TYPE_LEFT_MAP: Record<
|
||||||
OnboardingUserType,
|
OnboardingUserType,
|
||||||
@@ -105,7 +111,12 @@ const PREFLIGHT_LEFT = {
|
|||||||
"Freight Forwarders (Ethiopia & Djibouti)",
|
"Freight Forwarders (Ethiopia & Djibouti)",
|
||||||
"Transporters & Fleet Operators",
|
"Transporters & Fleet Operators",
|
||||||
],
|
],
|
||||||
stats: { label: "Active Customers", value: "500+", footer: "And growing", progress: "w-[95%]" },
|
stats: {
|
||||||
|
label: "Active Customers",
|
||||||
|
value: "500+",
|
||||||
|
footer: "And growing",
|
||||||
|
progress: "w-[95%]",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const DOCUMENT_SETTING_CODE_MAP: Record<OnboardingUserType, string> = {
|
const DOCUMENT_SETTING_CODE_MAP: Record<OnboardingUserType, string> = {
|
||||||
@@ -120,7 +131,9 @@ export default function OnboardingPage() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [userType, setUserType] = useState<OnboardingUserType | null>(null);
|
const [userType, setUserType] = useState<OnboardingUserType | null>(null);
|
||||||
const [documentFiles, setDocumentFiles] = useState<Record<string, File | File[] | null>>({});
|
const [documentFiles, setDocumentFiles] = useState<
|
||||||
|
Record<string, File | File[] | null>
|
||||||
|
>({});
|
||||||
|
|
||||||
const COMPANY_TYPE_MAP: Record<OnboardingUserType, string> = {
|
const COMPANY_TYPE_MAP: Record<OnboardingUserType, string> = {
|
||||||
importer: "customer",
|
importer: "customer",
|
||||||
@@ -131,7 +144,8 @@ export default function OnboardingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createCompanyMutation = useMutation({
|
const createCompanyMutation = useMutation({
|
||||||
mutationFn: (payload: CreateCompanyPayload) => api.companies.create.call(payload),
|
mutationFn: (payload: CreateCompanyPayload) =>
|
||||||
|
api.companies.create.call(payload),
|
||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
const hasFiles = Object.values(documentFiles).some(
|
const hasFiles = Object.values(documentFiles).some(
|
||||||
(f) => f !== null && (Array.isArray(f) ? f.length > 0 : true),
|
(f) => f !== null && (Array.isArray(f) ? f.length > 0 : true),
|
||||||
@@ -139,14 +153,19 @@ export default function OnboardingPage() {
|
|||||||
if (hasFiles) {
|
if (hasFiles) {
|
||||||
await companiesService.uploadDocuments(data.company.id, documentFiles);
|
await companiesService.uploadDocuments(data.company.id, documentFiles);
|
||||||
}
|
}
|
||||||
await queryClient.invalidateQueries({ queryKey: api.companies.getInfo.queryKey() });
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: api.companies.getInfo.queryKey(),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
const handleSubmit = (payload: CreateCompanyPayload) => {
|
const handleSubmit = (payload: CreateCompanyPayload) => {
|
||||||
const enriched: CreateCompanyPayload = { ...payload, companyType: COMPANY_TYPE_MAP[userType!] };
|
const enriched: CreateCompanyPayload = {
|
||||||
|
...payload,
|
||||||
|
companyType: COMPANY_TYPE_MAP[userType!],
|
||||||
|
};
|
||||||
createCompanyMutation.mutate(enriched);
|
createCompanyMutation.mutate(enriched);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,11 +228,28 @@ export default function OnboardingPage() {
|
|||||||
...leftConfig,
|
...leftConfig,
|
||||||
features:
|
features:
|
||||||
userType === "transporter"
|
userType === "transporter"
|
||||||
? ["Vehicle & fleet registration", "TIN & FAN verification", "First-mile / Last-mile eligibility"]
|
? [
|
||||||
|
"Vehicle & fleet registration",
|
||||||
|
"TIN & FAN verification",
|
||||||
|
"First-mile / Last-mile eligibility",
|
||||||
|
]
|
||||||
: userType === "freight-forwarder-dj"
|
: userType === "freight-forwarder-dj"
|
||||||
? ["Company details", "Representative information", "Cross-border operations"]
|
? [
|
||||||
: ["Company registration details", "Contact and management personnel", "Power of Attorney (optional)"],
|
"Company details",
|
||||||
stats: { label: "Active Customers", value: "500+", footer: "And growing", progress: "w-[95%]" },
|
"Representative information",
|
||||||
|
"Cross-border operations",
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
"Company registration details",
|
||||||
|
"Contact and management personnel",
|
||||||
|
"Power of Attorney (optional)",
|
||||||
|
],
|
||||||
|
stats: {
|
||||||
|
label: "Active Customers",
|
||||||
|
value: "500+",
|
||||||
|
footer: "And growing",
|
||||||
|
progress: "w-[95%]",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -71,10 +71,12 @@ export function DraftBookingView({
|
|||||||
).length;
|
).length;
|
||||||
const allDocsUploaded = uploadedCount === REQUIRED_DOC_FIELDS.length;
|
const allDocsUploaded = uploadedCount === REQUIRED_DOC_FIELDS.length;
|
||||||
|
|
||||||
const { data: generatedPricing } = useQuery({
|
const { data: generatedPricing } = useQuery(
|
||||||
...api.bookings.generatePrice.queryOptions({ input: { id: booking.id } }),
|
api.bookings.generatePrice.queryOptions({ input: { id: booking.id },
|
||||||
enabled: booking.status === "DRAFT" && !booking.pricingBreakdown,
|
|
||||||
});
|
enabled: booking.status === "DRAFT" && !booking.pricingBreakdown,
|
||||||
|
}),
|
||||||
|
);
|
||||||
const pricing = booking.pricingBreakdown ?? generatedPricing ?? null;
|
const pricing = booking.pricingBreakdown ?? generatedPricing ?? null;
|
||||||
|
|
||||||
const uploadMutation = useMutation({
|
const uploadMutation = useMutation({
|
||||||
|
|||||||
Reference in New Issue
Block a user