From 89cdc0ad0666305f6fd140a0d8811df8e2ef17a8 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Thu, 13 Aug 2026 13:46:46 +0000 Subject: [PATCH] style: ui fixes --- .../bookings/BookingActionsMenu.tsx | 14 +- .../bookings/BookingStatusBadge.tsx | 3 +- .../src/components/common/FilterToggle.tsx | 34 ++ .../contracts/ContractMilestonesTimeline.tsx | 5 +- .../contracts/ContractStatusTabs.tsx | 92 ----- .../src/components/customers/badges.tsx | 10 +- .../src/components/customers/format.ts | 40 +-- .../summary/OverviewActivityHeatmap.tsx | 2 +- .../summary/OverviewAttentionCard.tsx | 10 +- .../overview/summary/OverviewHero.tsx | 3 +- .../overview/summary/OverviewHeroKpis.tsx | 36 +- .../overview/summary/OverviewNetworkCard.tsx | 11 +- .../summary/OverviewPipelineFunnel.tsx | 2 +- .../overview/summary/OverviewRevenueMix.tsx | 2 +- .../summary/OverviewRevenueVolumeChart.tsx | 6 +- .../overview/summary/OverviewSankeyFlow.tsx | 2 +- .../src/components/warehouses/options.ts | 14 +- .../backoffice/src/lib/format.ts | 62 ++++ .../bookings/BookingRequestDetailPage.tsx | 5 +- .../pages/bookings/BookingRequestsPage.tsx | 128 +++---- .../bookings/DocumentClearanceListPage.tsx | 3 - .../src/pages/bookings/NewBookingPage.tsx | 3 +- .../pages/bookings/WagonCancellationsPage.tsx | 29 +- .../contracts/ClearanceDocumentsPage.tsx | 3 - .../contracts/ContractRequestDetailPage.tsx | 28 +- .../pages/contracts/ContractRequestsPage.tsx | 323 ++++++++---------- .../contracts/GlDjiboutiClearanceListPage.tsx | 16 +- .../pages/contracts/ShipmentRequestsPage.tsx | 2 +- .../src/pages/customers/CustomersPage.tsx | 48 +-- .../src/pages/fleet/DriverDetailPage.tsx | 12 +- .../src/pages/fleet/TrackingPage.tsx | 10 +- .../src/pages/fleet/VehicleDetailPage.tsx | 10 +- .../src/pages/invoices/InvoicesPage.tsx | 5 +- .../src/pages/invoices/UsdPaymentsPage.tsx | 5 +- .../operations/EdrTruckExitPapersModal.tsx | 7 +- .../src/pages/operations/FirstMilePage.tsx | 8 +- .../src/pages/operations/LastMilePage.tsx | 8 +- .../src/pages/payments/PaymentsPage.tsx | 25 +- .../settings/ExchangeRateSettingsCard.tsx | 3 +- .../pages/warehouses/ContainerReturnsPage.tsx | 3 +- .../src/pages/warehouses/ImportTrucksPage.tsx | 7 +- .../warehouses/InterchangeDocumentsPage.tsx | 3 +- .../src/pages/warehouses/TrucksOnSitePage.tsx | 3 +- .../warehouses/WarehouseInvoicesPage.tsx | 5 +- .../pages/warehouses/WarehouseRulesPage.tsx | 11 +- .../backoffice/src/types/overview.ts | 4 + 46 files changed, 417 insertions(+), 648 deletions(-) create mode 100644 apps/edr-freight-web/backoffice/src/components/common/FilterToggle.tsx delete mode 100644 apps/edr-freight-web/backoffice/src/components/contracts/ContractStatusTabs.tsx create mode 100644 apps/edr-freight-web/backoffice/src/lib/format.ts diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsMenu.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsMenu.tsx index b2d260e37..a50028e90 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsMenu.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsMenu.tsx @@ -1,5 +1,5 @@ import { useNavigate } from "react-router-dom"; -import { ChevronRight, ExternalLink, MoreHorizontal } from "lucide-react"; +import { ExternalLink, MoreHorizontal } from "lucide-react"; import { Button, Menu, ActionIcon, Group, Text } from "@mantine/core"; import { BookingConfirmDialog } from "./BookingConfirmDialog"; @@ -64,17 +64,9 @@ export function BookingActionsMenu({ const hasMenu = listRowHasActions(row, user); + // Row click already opens the detail page — no chevron affordance needed. if (!hasMenu && variant === "table") { - return ( - navigate(`/dashboard/booking-requests/${row.id}`)} - aria-label="View booking" - > - - - ); + return null; } // Toolbar: lay every action out as a button row. diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusBadge.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusBadge.tsx index 7d1fff022..fa21d0d9f 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusBadge.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusBadge.tsx @@ -1,6 +1,7 @@ import { Badge, Group } from "@mantine/core"; import { Link2 } from "lucide-react"; import { BOOKING_STATUS_STYLES } from "@/features/bookings/booking-status.config"; +import { humanize } from "@/lib/format"; const statusColorMap: Record = { DRAFT: "gray", @@ -40,7 +41,7 @@ export function BookingStatusBadge({ partnerReference, }: BookingStatusBadgeProps) { const style = BOOKING_STATUS_STYLES[status] ?? { - label: status, + label: humanize(status), color: "gray", }; const color = statusColorMap[status] ?? "gray"; diff --git a/apps/edr-freight-web/backoffice/src/components/common/FilterToggle.tsx b/apps/edr-freight-web/backoffice/src/components/common/FilterToggle.tsx new file mode 100644 index 000000000..b97f827d4 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/common/FilterToggle.tsx @@ -0,0 +1,34 @@ +import { ActionIcon, Indicator } from "@mantine/core"; +import { Filter } from "lucide-react"; + +export interface FilterToggleProps { + /** Number of active advanced filters — shown as a badge on the button. */ + count: number; + expanded: boolean; + onClick: () => void; +} + +/** Toggle for the collapsible advanced-filters row on list pages. */ +export function FilterToggle({ count, expanded, onClick }: FilterToggleProps) { + return ( + + + + + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/ContractMilestonesTimeline.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/ContractMilestonesTimeline.tsx index 1bf9b55e7..3a3c71449 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/ContractMilestonesTimeline.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/ContractMilestonesTimeline.tsx @@ -10,6 +10,7 @@ import { import { Group, Stack, Text, Timeline, Tooltip } from "@mantine/core"; import type { Freight } from "@edr/types"; +import { formatDate } from "@/lib/format"; import { CONTRACT_APPROVAL_ROLE_LABELS, HAZARDOUS_APPROVAL_ROLE_PERMISSION, @@ -54,10 +55,6 @@ function formatAgo(iso: string): string { return "just now"; } -function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString(undefined, { dateStyle: "medium" }); -} - type MilestoneIcon = typeof Send; interface Milestone { diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/ContractStatusTabs.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/ContractStatusTabs.tsx deleted file mode 100644 index 862bc1562..000000000 --- a/apps/edr-freight-web/backoffice/src/components/contracts/ContractStatusTabs.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Badge, ScrollArea, Tabs } from "@mantine/core"; -import { - ClipboardCheck, - FileSignature, - Inbox, - LayoutGrid, - ShieldCheck, - Truck, - XCircle, -} from "lucide-react"; - -import "@/components/overview/overview.css"; -import { - CONTRACT_LIST_TABS, - type ContractStatusTabKey, -} from "@/features/contracts/contract-status.config"; - -const TAB_ICONS: Record = { - all: , - intake: , - in_approval: , - approved_contract: , - clearance: , - active: , - closed: , -}; - -interface ContractStatusTabsProps { - active: ContractStatusTabKey; - onChange: (tab: ContractStatusTabKey) => void; - counts?: Partial>; -} - -export function ContractStatusTabs({ - active, - onChange, - counts, -}: ContractStatusTabsProps) { - return ( - onChange((value as ContractStatusTabKey) ?? "all")} - variant="pills" - color="edr-green" - keepMounted={false} - classNames={{ list: "ov-tablist", tab: "ov-tab" }} - > - - - {CONTRACT_LIST_TABS.map((tab) => { - const isActive = active === tab.key; - const count = counts?.[tab.key]; - return ( - - {count} - - ) : undefined - } - > - {tab.label} - - ); - })} - - - - ); -} - -export type { ContractStatusTabKey }; diff --git a/apps/edr-freight-web/backoffice/src/components/customers/badges.tsx b/apps/edr-freight-web/backoffice/src/components/customers/badges.tsx index edcc4a517..90dea5adb 100644 --- a/apps/edr-freight-web/backoffice/src/components/customers/badges.tsx +++ b/apps/edr-freight-web/backoffice/src/components/customers/badges.tsx @@ -116,8 +116,9 @@ export function CompanyNationalityBadge({ /** * Profile chips for a company row: one chip per role (Importer / Exporter / …) - * carrying its reference code. Caps at three (a company has at most three - * profiles); any extra collapse into a `+N` chip. + * carrying its reference code, colored by the profile's status (green active, + * amber pending, red rejected/blacklisted). Caps at three (a company has at + * most three profiles); any extra collapse into a `+N` chip. */ export function ProfileChips({ profiles, @@ -152,14 +153,15 @@ export function ProfileChips({ withArrow > - {humanize(profile.type)} · {profile.reference} + {humanize(profile.type)} + {profile.reference ? ` · ${profile.reference}` : ""} ))} diff --git a/apps/edr-freight-web/backoffice/src/components/customers/format.ts b/apps/edr-freight-web/backoffice/src/components/customers/format.ts index 0397c1cee..341170931 100644 --- a/apps/edr-freight-web/backoffice/src/components/customers/format.ts +++ b/apps/edr-freight-web/backoffice/src/components/customers/format.ts @@ -1,38 +1,2 @@ -/** Shared formatting helpers for the customer-management pages. */ - -/** snake_case / SCREAMING_CASE → Title Case. */ -export function humanize(value: string): string { - return value - .toLowerCase() - .split(/[_\s]+/) - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(" "); -} - -export function formatDate(value: string | null | undefined): string { - if (!value) return "—"; - const d = new Date(value); - return Number.isNaN(d.getTime()) - ? "—" - : d.toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - }); -} - -export function formatMoney(amount: number, currency: string): string { - return new Intl.NumberFormat(undefined, { - style: "currency", - currency, - maximumFractionDigits: 0, - }).format(amount); -} - -export function formatBytes(bytes: number): string { - if (!bytes) return "0 B"; - const units = ["B", "KB", "MB", "GB"]; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - const value = bytes / Math.pow(1024, i); - return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; -} +/** @deprecated import from "@/lib/format" (or ../../lib/format) instead. */ +export { humanize, formatDate, formatDateTime, formatMoney, formatBytes } from "../../lib/format"; diff --git a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewActivityHeatmap.tsx b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewActivityHeatmap.tsx index 4f1958265..7524c1f20 100644 --- a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewActivityHeatmap.tsx +++ b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewActivityHeatmap.tsx @@ -26,7 +26,7 @@ interface OverviewActivityHeatmapProps { * selected range. The bright cells (and the peak badge) are the hours the * intake team needs to be staffed for. */ -export function OverviewActivityHeatmap({ cells }: OverviewActivityHeatmapProps) { +export function OverviewActivityHeatmap({ cells = [] }: OverviewActivityHeatmapProps) { const countByCell = new Map(cells.map((c) => [`${c.dow}-${c.block}`, c.count])); const max = Math.max(0, ...cells.map((c) => c.count)); const peak = cells.reduce( diff --git a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewAttentionCard.tsx b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewAttentionCard.tsx index 142b8814f..c36db5e57 100644 --- a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewAttentionCard.tsx +++ b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewAttentionCard.tsx @@ -30,35 +30,35 @@ export function OverviewAttentionCard({ bookings, contracts, billing }: Overview { key: "needsAction", label: "Bookings needing action", - count: bookings.needsAction, + count: bookings.needsAction ?? 0, icon: AlertCircle, href: "/dashboard/booking-requests", }, { key: "urgent", label: "Urgent bookings", - count: bookings.urgent, + count: bookings.urgent ?? 0, icon: Clock, href: "/dashboard/booking-requests", }, { key: "contractsApproval", label: "Contracts in approval", - count: contracts.inApproval, + count: contracts.inApproval ?? 0, icon: FileSignature, href: "/dashboard/contract-requests", }, { key: "contractsClearance", label: "Contracts in clearance", - count: contracts.inClearance, + count: contracts.inClearance ?? 0, icon: ShieldCheck, href: "/dashboard/contracts/clearance", }, { key: "pendingPayments", label: "Pending payments", - count: billing.pendingPayments, + count: billing.pendingPayments ?? 0, icon: Banknote, href: "/dashboard/payments", }, diff --git a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewHero.tsx b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewHero.tsx index 04e2b273f..5b7815eb0 100644 --- a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewHero.tsx +++ b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewHero.tsx @@ -2,6 +2,7 @@ import { RefreshCw } from "lucide-react"; import { ActionIcon, Badge, Group, SegmentedControl, Stack, Text } from "@mantine/core"; import { useAuth } from "@/auth/useAuth"; +import { formatDateTime } from "@/lib/format"; import { freightBrand } from "@/theme/freight-brand"; import type { OverviewRange } from "@/types/overview"; import "@/components/overview/overview.css"; @@ -20,7 +21,7 @@ function formatRelativeTime(iso: string | undefined) { if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; - return new Date(iso).toLocaleString(); + return formatDateTime(iso); } function greeting(hour: number) { diff --git a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewHeroKpis.tsx b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewHeroKpis.tsx index 32ee768d1..fa250a817 100644 --- a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewHeroKpis.tsx +++ b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewHeroKpis.tsx @@ -34,21 +34,27 @@ interface OverviewHeroKpisProps { */ export function OverviewHeroKpis({ kpis, current, previous, rangeLabel }: OverviewHeroKpisProps) { const items: KpiItem[] = [ - { - label: `Revenue (${rangeLabel})`, - value: formatCurrency(n, "ETB")} />, - hint: formatCurrency(current.revenueUsd, "USD"), - icon: Banknote, - color: "yellow", - delta: pctDelta(current.revenueEtb, previous.revenueEtb), - }, - { - label: "Cargo moved", - value: `${Math.round(n).toLocaleString()} t`} />, - icon: Package, - color: "edr-green", - delta: pctDelta(current.tons, previous.tons), - }, + // An API deployed before the overview revamp omits the period totals — + // drop the two tiles that need them rather than crash (or hide the strip). + ...(current + ? [ + { + label: `Revenue (${rangeLabel})`, + value: formatCurrency(n, "ETB")} />, + hint: formatCurrency(current.revenueUsd, "USD"), + icon: Banknote, + color: "yellow", + delta: pctDelta(current.revenueEtb, previous?.revenueEtb ?? 0), + }, + { + label: "Cargo moved", + value: `${Math.round(n).toLocaleString()} t`} />, + icon: Package, + color: "edr-green", + delta: pctDelta(current.tons, previous?.tons ?? 0), + }, + ] + : []), { label: "Active bookings", value: , diff --git a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewNetworkCard.tsx b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewNetworkCard.tsx index 57a1a1fdc..9c9bef563 100644 --- a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewNetworkCard.tsx +++ b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewNetworkCard.tsx @@ -25,7 +25,10 @@ const STATS: Array<{ /** Network snapshot: four operational stats plus real wagon-utilization (available / total), not a decorative gauge. */ export function OverviewNetworkCard({ kpis }: { kpis: IOverviewOperationsKpis }) { - const utilizationPct = kpis.wagonsTotal > 0 ? (kpis.wagonsAvailable / kpis.wagonsTotal) * 100 : null; + // An API deployed before the overview revamp omits the wagon counters. + const wagonsTotal = kpis.wagonsTotal ?? 0; + const wagonsAvailable = kpis.wagonsAvailable ?? 0; + const utilizationPct = wagonsTotal > 0 ? (wagonsAvailable / wagonsTotal) * 100 : null; return ( - {kpis.wagonsAvailable.toLocaleString()} + {wagonsAvailable.toLocaleString()} {" "} - / {kpis.wagonsTotal.toLocaleString()} + / {wagonsTotal.toLocaleString()} @@ -72,7 +75,7 @@ export function OverviewNetworkCard({ kpis }: { kpis: IOverviewOperationsKpis }) /> - + {stat.label} diff --git a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewPipelineFunnel.tsx b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewPipelineFunnel.tsx index f28540298..a1c18c281 100644 --- a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewPipelineFunnel.tsx +++ b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewPipelineFunnel.tsx @@ -14,7 +14,7 @@ interface OverviewPipelineFunnelProps { } /** Booking pipeline by stage, in workflow order. Each row deep-links to the exact statuses it represents. */ -export function OverviewPipelineFunnel({ data }: OverviewPipelineFunnelProps) { +export function OverviewPipelineFunnel({ data = [] }: OverviewPipelineFunnelProps) { const rows = data .map((item) => ({ ...item, diff --git a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewRevenueMix.tsx b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewRevenueMix.tsx index c2839350b..0603ac095 100644 --- a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewRevenueMix.tsx +++ b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewRevenueMix.tsx @@ -111,7 +111,7 @@ interface OverviewRevenueMixProps { } /** ETB revenue split two ways — trade direction and freight type — anchored by the range total. */ -export function OverviewRevenueMix({ byDirection, byFreightType }: OverviewRevenueMixProps) { +export function OverviewRevenueMix({ byDirection = [], byFreightType = [] }: OverviewRevenueMixProps) { const total = byDirection.reduce((sum, s) => sum + s.amountEtb, 0); return ( diff --git a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewRevenueVolumeChart.tsx b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewRevenueVolumeChart.tsx index 12b20a45e..04a359e92 100644 --- a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewRevenueVolumeChart.tsx +++ b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewRevenueVolumeChart.tsx @@ -67,9 +67,9 @@ interface OverviewRevenueVolumeChartProps { * period" at a glance. */ export function OverviewRevenueVolumeChart({ - bookingTrend, - paymentTrend, - previousPaymentTrend, + bookingTrend = [], + paymentTrend = [], + previousPaymentTrend = [], rangeDays, }: OverviewRevenueVolumeChartProps) { const data = mergeTrend(bookingTrend, paymentTrend, previousPaymentTrend, rangeDays); diff --git a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewSankeyFlow.tsx b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewSankeyFlow.tsx index 1e4597ee9..b7974ea48 100644 --- a/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewSankeyFlow.tsx +++ b/apps/edr-freight-web/backoffice/src/components/overview/summary/OverviewSankeyFlow.tsx @@ -135,7 +135,7 @@ interface OverviewSankeyFlowProps { * freight type. Ribbon thickness is proportional to revenue, so the biggest * corridor is unmissable. */ -export function OverviewSankeyFlow({ flows }: OverviewSankeyFlowProps) { +export function OverviewSankeyFlow({ flows = [] }: OverviewSankeyFlowProps) { const data = toSankeyData(flows); return ( diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/options.ts b/apps/edr-freight-web/backoffice/src/components/warehouses/options.ts index 8b12d33e6..5fd393268 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/options.ts +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/options.ts @@ -99,18 +99,8 @@ export const formatDays = (value: number | null | undefined) => { return `${rounded} ${rounded === 1 ? 'day' : 'days'}`; }; -export const formatDate = (value: string | null | undefined) => { - if (!value) return '—'; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return '—'; - return date.toLocaleString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); -}; +// Despite the name, this has always rendered date + time — hence formatDateTime. +export { formatDateTime as formatDate } from '@/lib/format'; // Name fields (warehouse / fee rule / allocation rule) accept letters and spaces only — no numbers. export const lettersOnly = (value: string) => value.replace(/[^A-Za-z\s]/g, ''); diff --git a/apps/edr-freight-web/backoffice/src/lib/format.ts b/apps/edr-freight-web/backoffice/src/lib/format.ts new file mode 100644 index 000000000..4c5c6b639 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/lib/format.ts @@ -0,0 +1,62 @@ +/** Shared display-formatting helpers for the backoffice. */ + +/** snake_case / SCREAMING_CASE → Title Case. */ +export function humanize(value: string): string { + return value + .toLowerCase() + .split(/[_\s]+/) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + +export function formatDate(value: string | null | undefined): string { + if (!value) return "—"; + const d = new Date(value); + return Number.isNaN(d.getTime()) + ? "—" + : d.toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +export function formatDateTime(value: string | null | undefined): string { + if (!value) return "—"; + const d = new Date(value); + return Number.isNaN(d.getTime()) + ? "—" + : d.toLocaleString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +/** Pass `fractionDigits` where cents matter; the default matches the legacy whole-figure display. */ +export function formatMoney( + amount: number, + currency: string, + fractionDigits?: number, +): string { + return new Intl.NumberFormat(undefined, { + style: "currency", + currency, + ...(fractionDigits === undefined + ? { maximumFractionDigits: 0 } + : { + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits, + }), + }).format(amount); +} + +export function formatBytes(bytes: number): string { + if (!bytes) return "0 B"; + const units = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const value = bytes / Math.pow(1024, i); + return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; +} diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx index fdade7980..37609ea40 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx @@ -61,6 +61,7 @@ import { import { WarehouseInfoCard } from "@/components/warehouses"; import { getStatusMeta } from "@/features/bookings/booking-status.config"; import { toBookingListRow } from "@/features/bookings/mapBookingListRow"; +import { formatDateTime, formatMoney } from "@/lib/format"; import { cargoTonsAndItems } from "@/utils/cargoWeight"; import type { BookingDetail } from "@/types/booking"; import { @@ -186,7 +187,7 @@ export default function BookingRequestDetailPage() { const kpis: KpiItem[] = [ { label: "Total value", - value: `${booking.paymentCurrency} ${amount.toLocaleString(undefined, { minimumFractionDigits: 2 })}`, + value: formatMoney(amount, booking.paymentCurrency, 2), hint: booking.paymentStatus, icon: Wallet, color: "edr-green", @@ -326,7 +327,7 @@ export default function BookingRequestDetailPage() { {booking.holdExpiresAt && booking.schedulingStatus === "HOLDING" ? ( - Hold expires {new Date(booking.holdExpiresAt).toLocaleString()} + Hold expires {formatDateTime(booking.holdExpiresAt)} ) : null} diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx index d55c45c3a..4bd84a247 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx @@ -5,6 +5,7 @@ import { Button, Card, Checkbox, + Collapse, Group, Modal, MultiSelect, @@ -36,6 +37,8 @@ import { useNavigate, useSearchParams } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { BookingActionsMenu } from "@/components/bookings/BookingActionsMenu"; +import { FilterToggle } from "@/components/common/FilterToggle"; +import { formatDate, humanize } from "@/lib/format"; import { BookingPriorityBadge } from "@/components/bookings/BookingPriorityBadge"; import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge"; // BookingStatusTabs / Operations* queues removed — replaced by booking-kind tabs. @@ -50,7 +53,6 @@ import { useBookingList, useBookingListSummary, } from "@/hooks/bookings/useBookings"; -import { ContractReferenceLink } from "@/components/bookings/ContractReferenceLink"; import { api } from "@/services/api"; import type { BookingListFilter } from "@/services/bookings.service"; import { trainSchedulingService } from "@/services/trainScheduling.service"; @@ -116,18 +118,6 @@ function endOfDayIso(d: Date): string { return x.toISOString(); } -function formatDate(value: string | null | undefined): string { - if (!value) return "—"; - const d = new Date(value); - return Number.isNaN(d.getTime()) - ? "—" - : d.toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - }); -} - export default function BookingRequestsPage() { const navigate = useNavigate(); // Deep links land here pre-filtered (?statuses=A,B&tradeDirection=IMPORT) — @@ -159,6 +149,11 @@ export default function BookingRequestsPage() { const [createdTo, setCreatedTo] = useState(null); const [scheduledFrom, setScheduledFrom] = useState(null); const [scheduledTo, setScheduledTo] = useState(null); + // Direction is the only deep-linkable advanced filter — open the panel so a + // deep link never hides its own filter. + const [showAdvanced, setShowAdvanced] = useState(() => + Boolean(paramDirection), + ); const [allocateOpen, setAllocateOpen] = useState(false); const [allocateIds, setAllocateIds] = useState([]); // Paid bookings with no train attached (staff removed them or a sweep @@ -185,6 +180,7 @@ export default function BookingRequestsPage() { const next = paramStatuses.split(",").filter(Boolean); setStatusFilter((prev) => (prev.join(",") === next.join(",") ? prev : next)); setDirectionFilter(paramDirection); + if (paramDirection) setShowAdvanced(true); }, [paramStatuses, paramDirection]); const filter: BookingListFilter = useMemo(() => { @@ -278,6 +274,10 @@ export default function BookingRequestsPage() { (createdFrom || createdTo ? 1 : 0) + (scheduledFrom || scheduledTo ? 1 : 0); + // Badge on the advanced-filters toggle — active filters hidden behind it. + const advancedFilterCount = + activeFilterCount - (kindFilter ? 1 : 0) - (statusFilter.length ? 1 : 0); + const clearFilters = useCallback(() => { setKindFilter(null); setStatusFilter([]); @@ -390,13 +390,22 @@ export default function BookingRequestsPage() { header: () => Booking, cell: ({ row }) => { const b = row.original; + const isGeneral = b.bookingKind === "GENERAL_CONTRACT"; return (
-
-

{b.reference}

+
+
+

{b.reference}

+ + {isGeneral ? "General" : "One-time"} + +

{b.customerLabel} @@ -406,49 +415,6 @@ export default function BookingRequestsPage() { ); }, }, - { - id: "contract", - header: () => Contract, - cell: ({ row }) => { - const ref = row.original.contractReference; - return ( -

- {ref ? ( - // Fall back to plain text when the id is missing — the reference is - // still worth showing, it just has nowhere to link to. - (row.original.contractId ? ( - - ) : ( - {ref} - )) - ) : ( - - )} -
- ); - }, - }, - { - id: "bookingKind", - header: () => Type, - cell: ({ row }) => { - const isGeneral = row.original.bookingKind === "GENERAL_CONTRACT"; - return ( -
- - {isGeneral ? "General" : "One-time"} - -
- ); - }, - }, { id: "route", header: () => Route, @@ -464,15 +430,15 @@ export default function BookingRequestsPage() {
- {b.tradeDirection} + {humanize(b.tradeDirection)} - {b.freightType} + {humanize(b.freightType)}
@@ -621,7 +587,7 @@ export default function BookingRequestsPage() { - + } @@ -649,11 +615,6 @@ export default function BookingRequestsPage() { style={{ flex: 1, minWidth: "200px" }} radius="lg" /> - - {total} record{total !== 1 ? "s" : ""} - - - - - (); const navigate = useNavigate(); diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx index dfbb26d25..cade8cc34 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx @@ -5,6 +5,7 @@ import { Box, Button, Card, + Collapse, Group, MultiSelect, Select, @@ -32,29 +33,25 @@ import { User, X, } from "lucide-react"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo, useState, type ReactNode } from "react"; import { useNavigate } from "react-router-dom"; -import { ContractApprovalProgressCell } from "@/components/contracts/ContractApprovalProgressCell"; -import { - ContractCourtBadge, - ContractStatusBadge, -} from "@/components/contracts/ContractStatusBadge"; -import { - ContractStatusTabs, - type ContractStatusTabKey, -} from "@/components/contracts/ContractStatusTabs"; +import { ContractStatusBadge } from "@/components/contracts/ContractStatusBadge"; +import { FilterToggle } from "@/components/common/FilterToggle"; import { bookingTable } from "@/components/bookings/booking-ui.styles"; import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; import { CONTRACT_LIST_TABS, CONTRACT_STATUS_STYLES, + contractCourt, } from "@/features/contracts/contract-status.config"; +import { formatContractApprovalProgress } from "@/features/contracts/contract-approval-progress"; import { getStaffRowAction, toContractListRow, type ContractListRow, } from "@/features/contracts/mapContractListRow"; +import { formatDate, humanize } from "@/lib/format"; import { useContractList, useContractListSummary, @@ -68,25 +65,13 @@ import { type ColumnDef, } from "@edr/ui-common"; -function getStatusesForTab(tab: ContractStatusTabKey): string | undefined { - const match = CONTRACT_LIST_TABS.find((t) => t.key === tab); - if (!match?.statuses?.length) return undefined; - return match.statuses.join(","); -} - -/** Statuses selectable in the status filter for a given tab ("all" → every tab status). */ -function getStatusOptionsForTab( - tab: ContractStatusTabKey, -): { value: string; label: string }[] { - const match = CONTRACT_LIST_TABS.find((t) => t.key === tab); - const statuses = match?.statuses?.length - ? match.statuses - : CONTRACT_LIST_TABS.flatMap((t) => t.statuses ?? []); - return statuses.map((s) => ({ +/** Every filterable status — the pill tabs are gone, so the select carries them all. */ +const STATUS_OPTIONS = CONTRACT_LIST_TABS.flatMap((t) => t.statuses ?? []).map( + (s) => ({ value: s, label: CONTRACT_STATUS_STYLES[s]?.label ?? s, - })); -} + }), +); const TRADE_DIRECTION_OPTIONS = [ { value: "IMPORT", label: "Import" }, @@ -117,8 +102,11 @@ const SORT_OPTIONS = [ { value: "contractValidUntil:DESC", label: "Expiring latest" }, ]; -/** Every column is 120px wide and wraps its content instead of truncating. */ -const COLUMN_WIDTH = 120; +/** + * Per-column widths — they must sum to the table's min-w (960px, set on the + * containerClassName below) because table-fixed distributes any difference. + */ +const COLUMN_WIDTHS = { contract: 250, route: 270, status: 250, validity: 190 }; const COLUMN_META = { headerClassName: "whitespace-normal break-words", cellClassName: "whitespace-normal break-words align-top", @@ -138,24 +126,11 @@ function endOfDayIso(d: Date): string { return x.toISOString(); } -function formatDate(value: string | null | undefined): string { - if (!value) return "—"; - const d = new Date(value); - return Number.isNaN(d.getTime()) - ? "—" - : d.toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - }); -} - export default function ContractRequestsPage() { const navigate = useNavigate(); const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [query, setQuery] = useState(""); const [debouncedQuery] = useDebouncedValue(query, 300); - const [activeTab, setActiveTab] = useState("all"); // Filter controls (empty/null = "all"). const [statusFilter, setStatusFilter] = useState([]); const { filterOptions } = useMyTradeAccess(); @@ -168,12 +143,8 @@ export default function ContractRequestsPage() { const [createdFrom, setCreatedFrom] = useState(null); const [createdTo, setCreatedTo] = useState(null); const [sort, setSort] = useState("createdAt:DESC"); - - const tabStatuses = getStatusesForTab(activeTab); - const statusOptions = useMemo( - () => getStatusOptionsForTab(activeTab), - [activeTab], - ); + // All filters start empty (no URL params on this page), so collapsed is safe. + const [showAdvanced, setShowAdvanced] = useState(false); const resetPage = useCallback(() => { setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); @@ -186,16 +157,11 @@ export default function ContractRequestsPage() { pageSize: pagination.pageSize, sortBy, sortOrder, - tab: activeTab, + // Kept as the React Query cache-key discriminator (tabs themselves are gone). + tab: "all", // Server-side free-text search (contract reference, customer name). ...(debouncedQuery.trim() ? { search: debouncedQuery.trim() } : {}), - // Explicit status picks narrow within the tab; otherwise the tab's - // status group applies. - ...(statusFilter.length - ? { statuses: statusFilter.join(",") } - : tabStatuses - ? { statuses: tabStatuses } - : {}), + ...(statusFilter.length ? { statuses: statusFilter.join(",") } : {}), ...(directionFilter ? { tradeDirection: directionFilter } : {}), ...(freightTypeFilter ? { freightType: freightTypeFilter } : {}), ...(kindFilter ? { contractKind: kindFilter } : {}), @@ -206,8 +172,6 @@ export default function ContractRequestsPage() { }, [ pagination.pageIndex, pagination.pageSize, - activeTab, - tabStatuses, debouncedQuery, statusFilter, directionFilter, @@ -227,6 +191,10 @@ export default function ContractRequestsPage() { (currencyFilter ? 1 : 0) + (createdFrom || createdTo ? 1 : 0); + // Badge on the advanced-filters toggle — active filters hidden behind it. + const advancedFilterCount = + activeFilterCount - (statusFilter.length ? 1 : 0) - (kindFilter ? 1 : 0); + const clearFilters = useCallback(() => { setStatusFilter([]); setDirectionFilter(null); @@ -273,7 +241,7 @@ export default function ContractRequestsPage() { const columns: ColumnDef[] = [ { id: "contract", - size: COLUMN_WIDTH, + size: COLUMN_WIDTHS.contract, meta: COLUMN_META, header: () => Customer, cell: ({ row }) => { @@ -296,7 +264,7 @@ export default function ContractRequestsPage() { }, { id: "route", - size: COLUMN_WIDTH, + size: COLUMN_WIDTHS.route, meta: COLUMN_META, header: () => Route, cell: ({ row }) => { @@ -311,7 +279,7 @@ export default function ContractRequestsPage() {
{directionLabel(c.tradeDirection)} @@ -319,7 +287,7 @@ export default function ContractRequestsPage() { variant="secondary" className="h-5 bg-muted/40 px-1.5 text-[10px] font-medium" > - {c.freightType} + {humanize(c.freightType)}
@@ -328,47 +296,77 @@ export default function ContractRequestsPage() { }, { id: "status", - size: COLUMN_WIDTH, + size: COLUMN_WIDTHS.status, meta: COLUMN_META, header: () => Status, - cell: ({ row }) => ( -
- -
- ), - }, - { - id: "court", - size: COLUMN_WIDTH, - meta: COLUMN_META, - header: () => ( - Waiting on - ), - cell: ({ row }) => ( -
- -
- ), - }, - { - id: "approval", - size: COLUMN_WIDTH, - meta: COLUMN_META, - header: () => Approval, - cell: ({ row }) => , + cell: ({ row }) => { + const c = row.original; + const court = contractCourt(c.status); + const progress = formatContractApprovalProgress( + c.status, + c.approvalSteps, + ); + const action = getStaffRowAction(c); + // One dimmed line: who it's waiting on, the staff verb, and the + // approval chain when one exists. "Open" adds nothing — row click + // already opens the detail page. + const pieces: ReactNode[] = []; + if (court) { + pieces.push(court === "customer" ? "With customer" : "With EDR"); + } + if (action && action.variant === "filled") { + pieces.push( + // "View & sign" pointed at the contract-view page, not the + // detail page — keep that deep link as an inline link. + action.to(c.id).endsWith("/view") ? ( + + ) : ( + action.label + ), + ); + } + if ((c.approvalSteps ?? []).length > 0) { + pieces.push(progress.label); + if (!progress.complete && progress.detail.startsWith("Next:")) { + pieces.push(progress.detail); + } + } + return ( +
+ + {pieces.length ? ( +
+ {pieces.map((piece, i) => ( + + {i > 0 ? " · " : null} + {piece} + + ))} +
+ ) : null} +
+ ); + }, }, { id: "validity", - size: COLUMN_WIDTH, + size: COLUMN_WIDTHS.validity, meta: COLUMN_META, header: () => Validity, cell: ({ row }) => { const c = row.original; + const isGeneral = c.contractKind === "GENERAL"; return ( - + {c.validUntil @@ -382,58 +380,22 @@ export default function ContractRequestsPage() { From {formatDate(c.validFrom)} ) : null} + + {isGeneral ? ( + + General + + ) : ( + "One-time" + )} + ); }, }, - { - id: "kind", - size: COLUMN_WIDTH, - meta: COLUMN_META, - header: () => Kind, - cell: ({ row }) => { - const isGeneral = row.original.contractKind === "GENERAL"; - return ( - - {isGeneral ? ( - - General - - ) : ( - "One-time" - )} - - ); - }, - }, - { - id: "actions", - size: COLUMN_WIDTH, - meta: COLUMN_META, - header: () => Action, - cell: ({ row }) => { - const action = getStaffRowAction(row.original); - if (!action) return null; - return ( - - ); - }, - }, ]; return ( @@ -486,22 +448,11 @@ export default function ContractRequestsPage() { ]} /> - { - setActiveTab(tab); - // Status picks belong to the previous tab's option set — reset. - setStatusFilter([]); - setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); - }} - counts={tabCounts} - /> - - + } @@ -541,16 +492,11 @@ export default function ContractRequestsPage() { style={{ minWidth: 170 }} aria-label="Sort contracts" /> - - {total} record{total !== 1 ? "s" : ""} - - - { setStatusFilter(v); @@ -562,6 +508,38 @@ export default function ContractRequestsPage() { style={{ minWidth: 220 }} aria-label="Filter by status" /> + - - {activeFilterCount > 0 ? ( - - ) : null} + @@ -672,7 +627,7 @@ export default function ContractRequestsPage() { manualPagination: true, pageCount, }} - // table-fixed makes the per-column 120px widths stick; without + // table-fixed makes the per-column widths stick; without // it auto-layout re-widens columns once cells wrap. containerClassName="border-0 shadow-none bg-transparent [&_table]:table-fixed [&_table]:min-w-[960px]" footer={DataTableFooter} diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx index 77ad33f56..76f2bc5b0 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx @@ -1,3 +1,4 @@ +import { formatDate } from "@/lib/format"; import { directionLabel } from "@/lib/utils"; import { useCallback, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -51,18 +52,6 @@ const prettyStatus = (s?: string | null) => .replace(/_/g, " ") .replace(/^\w/, (c) => c.toUpperCase()); -function formatDate(value?: string | null): string { - if (!value) return "—"; - const d = new Date(value); - return Number.isNaN(d.getTime()) - ? "—" - : d.toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - }); -} - function yardLabel( yard?: { label?: string; code?: string; name?: string } | null, fallback = "—", @@ -661,9 +650,6 @@ export default function GlDjiboutiClearanceListPage() { Clear ) : null} - - {total} record{total !== 1 ? "s" : ""} - diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ShipmentRequestsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ShipmentRequestsPage.tsx index 81b037ae6..73b9907f5 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/ShipmentRequestsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ShipmentRequestsPage.tsx @@ -294,7 +294,7 @@ export default function ShipmentRequestsPage() { {row.original.contractReference} {row.original.customerName ? ( - + {row.original.customerName} ) : null} diff --git a/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx b/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx index 815615a1b..23120f98f 100644 --- a/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx @@ -147,7 +147,7 @@ export default function CustomersPage() {
- + {c.name} @@ -156,6 +156,9 @@ export default function CustomersPage() { TIN {c.tin} {c.country ? ` · ${c.country}` : ""} + + +
); @@ -164,16 +167,9 @@ export default function CustomersPage() { { id: "profiles", header: "Profiles", - cell: ({ row }) => ( - - ), - }, - { - id: "status", - header: "Status", cell: ({ row }) => { - // A draft's profiles are all `pending` by construction, so the - // "N pending" review hint would be a lie until they submit. + // A draft's profiles are all `pending` by construction — the + // status-colored chips would be a lie until they submit. if (isOnboardingDraft(row.original)) { return ( @@ -183,23 +179,7 @@ export default function CustomersPage() { ); } - const pending = (row.original.companyProfiles ?? []).filter( - (p) => p.status === "pending", - ).length; - return ( - - - {pending > 0 ? ( - 1 ? "s" : ""} awaiting approval`} - > - - {pending} pending - - - ) : null} - - ); + return ; }, }, { @@ -229,6 +209,7 @@ export default function CustomersPage() { c="dimmed" className="inline-flex items-center gap-1" truncate + maw={200} > {c.email}
@@ -247,16 +228,6 @@ export default function CustomersPage() { ), }, - { - id: "approved", - header: "Approved", - meta: { headerClassName: "text-right", cellClassName: "text-right" }, - cell: ({ row }) => ( - - {row.original.approvedAt ? formatDate(row.original.approvedAt) : "—"} - - ), - }, ], [], ); @@ -376,9 +347,6 @@ export default function CustomersPage() { }} data={SORT_OPTIONS.map((o) => ({ ...o }))} /> - - {total} record{total !== 1 ? "s" : ""} - diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/DriverDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/DriverDetailPage.tsx index 3633b1942..9e8105c54 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/DriverDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/DriverDetailPage.tsx @@ -43,17 +43,13 @@ import { fetchViewableFile, } from "@/services/files.service"; import { useToast } from "@/hooks/use-toast"; +import { formatDateTime } from "@/lib/format"; const fmtDate = (iso?: string | null) => { if (!iso) return "—"; const d = new Date(iso); return Number.isNaN(d.getTime()) ? "—" : d.toLocaleDateString(); }; -const fmtDateTime = (iso?: string | null) => { - if (!iso) return "—"; - const d = new Date(iso); - return Number.isNaN(d.getTime()) ? "—" : d.toLocaleString(); -}; const meta = (e: FleetHistoryEvent, k: string) => { const v = e.metadata?.[k]; return typeof v === "string" && v ? v : null; @@ -400,7 +396,7 @@ const VehiclesTab = ({ driverId }: { driverId: string }) => { {rows.map((r) => ( {r.plate} - {fmtDateTime(r.at)} + {formatDateTime(r.at)} ))} @@ -425,7 +421,7 @@ const HistoryTab = ({ driverId }: { driverId: string }) => { .join(" · ")} )} - {fmtDateTime(e.createdAt)} + {formatDateTime(e.createdAt)} ))} @@ -471,7 +467,7 @@ const TripsTab = ({ driverId }: { driverId: string }) => { {t.booking} {t.vehicle} {t.status} - {fmtDateTime(t.at)} + {formatDateTime(t.at)} ))} diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/TrackingPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/TrackingPage.tsx index cb64d3b22..bcfd9731a 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/TrackingPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/TrackingPage.tsx @@ -35,6 +35,7 @@ import { type GpsDevice, } from "@/services/gps-tracking.service"; import { freightBrand } from "@/theme/freight-brand"; +import { formatDateTime } from "@/lib/format"; // Maps JavaScript API keys are public client-side keys — lock them down by // HTTP-referrer in the Google Cloud console. No fallback: a hardcoded default @@ -51,11 +52,6 @@ const deviceLabel = (d: GpsDevice) => ? [d.vehicle.code, d.vehicle.plateNumber].filter(Boolean).join(" · ") : d.name || d.imei; -const fmtTime = (iso?: string | null) => { - if (!iso) return "—"; - const d = new Date(iso); - return Number.isNaN(d.getTime()) ? "—" : d.toLocaleString(); -}; const StatBox = ({ label, value }: { label: string; value: string }) => ( @@ -530,7 +526,7 @@ export function TrackingPage() { Last fix - {fmtTime(selected.lastFixAt)} + {formatDateTime(selected.lastFixAt)}
{selected.vehicleId && ( @@ -580,7 +576,7 @@ export function TrackingPage() {
{toNum(d.lastSpeed) ?? 0} km/h ·{" "} - {fmtTime(d.lastFixAt)} + {formatDateTime(d.lastFixAt)}
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/VehicleDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/VehicleDetailPage.tsx index ae785a708..bcfadb698 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/VehicleDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/VehicleDetailPage.tsx @@ -42,6 +42,7 @@ import { } from "@/services/vehicles.service"; import { driversService } from "@/services/drivers.service"; import { fleetHistoryService } from "@/services/fleet-history.service"; +import { formatDateTime } from "@/lib/format"; interface MaintenanceCost { id: string; @@ -77,11 +78,6 @@ const fmtDate = (iso?: string | null) => { const d = new Date(iso); return Number.isNaN(d.getTime()) ? "—" : d.toLocaleDateString(); }; -const fmtDateTime = (iso?: string | null) => { - if (!iso) return "—"; - const d = new Date(iso); - return Number.isNaN(d.getTime()) ? "—" : d.toLocaleString(); -}; const money = (n?: number | null) => n == null ? "—" : `ETB ${Number(n).toLocaleString(undefined, { maximumFractionDigits: 2 })}`; @@ -359,7 +355,7 @@ const DriverTab = ({ {drivers.map((d) => ( {d.name} - {fmtDateTime(d.at)} + {formatDateTime(d.at)} ))} @@ -388,7 +384,7 @@ const HistoryTab = ({ vehicleId }: { vehicleId: string }) => { .join(" · ")} )} - {fmtDateTime(e.createdAt)} + {formatDateTime(e.createdAt)} ))} diff --git a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx index afa4fa603..9b0fb9392 100644 --- a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx @@ -73,7 +73,7 @@ export default function InvoicesPanel() { id: "billedTo", header: "Billed to", cell: ({ row }) => ( - + {row.original.company?.name ?? "—"} ), @@ -170,9 +170,6 @@ export default function InvoicesPanel() { { label: "Overdue", value: "OVERDUE" }, ]} /> - - {total} record{total !== 1 ? "s" : ""} - ( - + {row.original.company?.name ?? "—"} ), @@ -304,9 +304,6 @@ export default function UsdPaymentsPanel() { { label: "Overdue", value: "OVERDUE" }, ]} /> - - {total} record{total !== 1 ? "s" : ""} - - value ? new Date(value).toLocaleString() : "—"; /** * Per-truck exit papers for an EDR last-mile delivery. Each assigned truck has @@ -87,11 +86,11 @@ export function EdrTruckExitPapersModal({ opened, onClose, record }: EdrTruckExi {load} - {fmt(t.arrivedAt)} + {formatDateTime(t.arrivedAt)} {t.departedAt ? ( - {fmt(t.departedAt)} + {formatDateTime(t.departedAt)} ) : ( Still on site diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 192a748c3..ae9bd3b21 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -1139,7 +1139,11 @@ const FirstMilePage = () => { id: "customer", header: "Customer", meta: { headerClassName, cellClassName }, - cell: ({ row }) => customerName(row.original), + cell: ({ row }) => ( + + {customerName(row.original)} + + ), }, { id: "postPayment", @@ -1189,7 +1193,7 @@ const FirstMilePage = () => { id: "exactKm", header: "Actual Distance (KM)", meta: { headerClassName, cellClassName }, - cell: ({ row }) => row.original.exactKm != null ? row.original.exactKm : , + cell: ({ row }) => row.original.exactKm != null ? `${row.original.exactKm} km` : , }, { id: "invoice", diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx index 99c6e0416..9f7198609 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -1296,7 +1296,11 @@ const LastMilePage = () => { id: "customer", header: "Customer", meta: { headerClassName, cellClassName }, - cell: ({ row }) => customerName(row.original), + cell: ({ row }) => ( + + {customerName(row.original)} + + ), }, { id: "postPayment", @@ -1346,7 +1350,7 @@ const LastMilePage = () => { id: "exactKm", header: "Actual Distance (KM)", meta: { headerClassName, cellClassName }, - cell: ({ row }) => row.original.exactKm != null ? row.original.exactKm : , + cell: ({ row }) => row.original.exactKm != null ? `${row.original.exactKm} km` : , }, { id: "invoice", diff --git a/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx index 2d98cc805..a560b3291 100644 --- a/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx @@ -8,7 +8,6 @@ import { Select, Stack, Tabs, - Text, TextInput, } from "@mantine/core"; import { @@ -26,6 +25,7 @@ import { useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { KpiStrip } from "@/components/page"; +import { formatDate, formatMoney } from "@/lib/format"; import { api } from "@/services/api"; import type { PaymentMethod, PaymentRow } from "@/services/payments.service"; import { @@ -80,24 +80,6 @@ const STATUS_COLORS: Record = { refunded: "indigo", }; -function formatAmount(amount: number, currency: string): string { - return `${currency} ${Number(amount).toLocaleString(undefined, { - minimumFractionDigits: 2, - })}`; -} - -function formatDate(iso: string | null): string { - if (!iso) return "—"; - const d = new Date(iso); - return Number.isNaN(d.getTime()) - ? "—" - : d.toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - }); -} - const tableHeader = "text-xs font-semibold uppercase tracking-wide text-muted-foreground"; @@ -166,7 +148,7 @@ export default function PaymentsPanel() { header: () => Amount, cell: ({ row }) => ( - {formatAmount(row.original.amount, row.original.currency)} + {formatMoney(row.original.amount, row.original.currency, 2)} ), }, @@ -327,9 +309,6 @@ export default function PaymentsPanel() { }} style={{ minWidth: 180 }} /> - - {total} record{total !== 1 ? "s" : ""} - diff --git a/apps/edr-freight-web/backoffice/src/pages/settings/ExchangeRateSettingsCard.tsx b/apps/edr-freight-web/backoffice/src/pages/settings/ExchangeRateSettingsCard.tsx index 8527879fc..da01b51ce 100644 --- a/apps/edr-freight-web/backoffice/src/pages/settings/ExchangeRateSettingsCard.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/settings/ExchangeRateSettingsCard.tsx @@ -15,6 +15,7 @@ import { useSetExchangeFallbackRate, } from "@/hooks/useExchangeSettings"; import type { ExchangeRateSource } from "@/services/exchangeSettings.service"; +import { formatDateTime } from "@/lib/format"; /** Feed health, phrased for an operator rather than a developer. */ function feedLabel(source: ExchangeRateSource | null): { @@ -35,7 +36,7 @@ function feedLabel(source: ExchangeRateSource | null): { } const formatTime = (value: string | null) => - value ? new Date(value).toLocaleString() : "never"; + value ? formatDateTime(value) : "never"; /** * USD→ETB fallback used when the CBE exchange-rate endpoint is unreachable. diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx index 6f329c8d7..f8468181c 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx @@ -37,6 +37,7 @@ import type { EmptyContainerReturn, EmptyContainerReturnStatus, } from "@/types/importOperations"; +import { formatDateTime } from "@/lib/format"; type ReturnType = "all" | "edr" | "customer"; @@ -635,7 +636,7 @@ export default function ContainerReturnsPage() { {(historyRow.statusHistory ?? []).map((entry: any, idx: number) => ( {RETURN_STATUS_LABEL[entry.status as EmptyContainerReturnStatus] ?? entry.status} - {new Date(entry.changedAt).toLocaleString()} + {formatDateTime(entry.changedAt)} ))} {!(historyRow.statusHistory ?? []).length && ( diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/ImportTrucksPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/ImportTrucksPage.tsx index d2cc909f2..a074006d8 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/ImportTrucksPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/ImportTrucksPage.tsx @@ -38,6 +38,7 @@ import { toReleaseInventoryItem, } from "@/components/warehouses/options"; import { openPdfBlob } from "@/components/warehouses/pdf"; +import { formatDateTime } from "@/lib/format"; import { useListControls } from "@/hooks/useListControls"; import { useToast } from "@/hooks/use-toast"; import { api } from "@/services/api"; @@ -79,8 +80,6 @@ const TRUCK_COLUMNS = [ const money = (amount: number, currency: string) => `${Number(amount).toLocaleString(undefined, { maximumFractionDigits: 2 })} ${currency === "ETB" ? "ETB" : currency}`; -const formatTime = (iso: string | null | undefined) => - iso ? new Date(iso).toLocaleString() : "—"; export interface BookingGroup { bookingId: string; @@ -376,10 +375,10 @@ export function TruckRows({ group }: { group: BookingGroup }) { {t.containers.length ? t.containers.join(", ") : "Bulk"} - {formatTime(t.arrivedAt)} + {formatDateTime(t.arrivedAt)} - {formatTime(t.departedAt)} + {formatDateTime(t.departedAt)} {formatNumber(t.weight)} {money(t.demurrage, feeCurrency)} diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/InterchangeDocumentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/InterchangeDocumentsPage.tsx index 3cc1f1ab6..fa2c704b2 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/InterchangeDocumentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/InterchangeDocumentsPage.tsx @@ -28,6 +28,7 @@ import { useInterchangeDocuments, } from '@/hooks/useInterchangeDocuments'; import { useToast } from '@/hooks/use-toast'; +import { humanize } from '@/lib/format'; import { interchangeDocumentsService } from '@/services/interchange-documents.service'; import type { InterchangeDocument, InterchangeDocumentStatus } from '@/types/interchangeDocument'; @@ -336,7 +337,7 @@ export default function InterchangeDocumentsPage() { ), }, - { id: 'direction', header: 'Direction', cell: ({ row }) => row.original.direction }, + { id: 'direction', header: 'Direction', cell: ({ row }) => humanize(row.original.direction) }, { id: 'train', header: 'Train No', cell: ({ row }) => row.original.trainNo ?? '-' }, { id: 'handoverLocation', header: 'Handover Location', cell: ({ row }) => row.original.handoverLocation }, { id: 'handoverFrom', header: 'Handover From', cell: ({ row }) => row.original.handoverFrom }, diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/TrucksOnSitePage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/TrucksOnSitePage.tsx index f095d94d4..d30f9144b 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/TrucksOnSitePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/TrucksOnSitePage.tsx @@ -18,6 +18,7 @@ import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter"; import { useListControls } from "@/hooks/useListControls"; import { useTrucksOnSite } from "@/hooks/useWarehouses"; import type { TruckOnSite } from "@/types/warehouse"; +import { formatDateTime } from "@/lib/format"; /** * Every truck inside the yard right now, across all bookings. @@ -122,7 +123,7 @@ function Rows({ rows }: { rows: TruckOnSite[] }) { ) : isLongDwell(row.arrivedAt) ? ( diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx index dc4ae4f86..17becfaa6 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx @@ -40,6 +40,7 @@ import { buildWarehouseExitPaperPdf } from '@/components/warehouses/warehousePdf import { extractErrorMessage } from '@/components/warehouses/options'; import { useAuth } from '@/auth/useAuth'; import { FREIGHT_PERMS, hasPermission } from '@/lib/permissions'; +import { formatMoney, humanize } from '@/lib/format'; const STATUS_COLOR: Record = { DRAFT: 'gray', @@ -50,7 +51,7 @@ const STATUS_COLOR: Record = { CANCELLED: 'gray', }; -const fmt = (n: number, c: string) => `${Number(n).toLocaleString()} ${c === 'ETB' ? 'Birr (ETB)' : c}`; +const fmt = (n: number, c: string) => formatMoney(n, c, 2); const fmtDate = (d?: string | null) => (d ? new Date(d).toLocaleDateString() : '—'); export default function WarehouseInvoicesPage() { @@ -79,7 +80,7 @@ export default function WarehouseInvoicesPage() { ), }, - { id: 'type', header: 'Type', cell: ({ row }) => row.original.invoiceType.replace(/_/g, ' ') }, + { id: 'type', header: 'Type', cell: ({ row }) => humanize(row.original.invoiceType) }, { id: 'total', header: 'Total', cell: ({ row }) => fmt(row.original.totalAmount, row.original.currency) }, { id: 'paid', header: 'Paid', cell: ({ row }) => fmt(row.original.paidAmount, row.original.currency) }, { diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx index a6339965a..8c432f92a 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx @@ -46,6 +46,7 @@ import { type FeeRuleBasis, type FeeRuleType, } from '@/types/warehouse'; +import { humanize } from '@/lib/format'; const RULE_TYPE_COLOR: Record = { STORAGE_FEE: 'teal', @@ -218,8 +219,8 @@ function AllocationRules() { const allocationColumns: ColumnDef[] = [ { id: 'priority', header: 'Priority', cell: ({ row }) => row.original.priority }, { id: 'name', header: 'Name', cell: ({ row }) => row.original.name }, - { id: 'freight', header: 'Freight', cell: ({ row }) => row.original.freightType ?? dash }, - { id: 'trade', header: 'Trade', cell: ({ row }) => row.original.tradeDirection ?? dash }, + { id: 'freight', header: 'Freight', cell: ({ row }) => (row.original.freightType ? humanize(row.original.freightType) : dash) }, + { id: 'trade', header: 'Trade', cell: ({ row }) => (row.original.tradeDirection ? humanize(row.original.tradeDirection) : dash) }, { id: 'cargoCode', header: 'Cargo code', cell: ({ row }) => row.original.cargoTypeCode ?? dash }, { id: 'targetYard', @@ -615,10 +616,10 @@ function FeeRules() { ), }, { id: 'name', header: 'Name', cell: ({ row }) => row.original.name }, - { id: 'freight', header: 'Freight', cell: ({ row }) => row.original.freightType ?? dash }, - { id: 'trade', header: 'Trade', cell: ({ row }) => row.original.tradeDirection ?? dash }, + { id: 'freight', header: 'Freight', cell: ({ row }) => (row.original.freightType ? humanize(row.original.freightType) : dash) }, + { id: 'trade', header: 'Trade', cell: ({ row }) => (row.original.tradeDirection ? humanize(row.original.tradeDirection) : dash) }, { id: 'cargo', header: 'Cargo', cell: ({ row }) => row.original.cargoTypeCode ?? dash }, - { id: 'container', header: 'Container', cell: ({ row }) => row.original.containerType ?? dash }, + { id: 'container', header: 'Container', cell: ({ row }) => (row.original.containerType ? humanize(row.original.containerType) : dash) }, { id: 'scope', header: 'Location scope', diff --git a/apps/edr-freight-web/backoffice/src/types/overview.ts b/apps/edr-freight-web/backoffice/src/types/overview.ts index f0553fc22..38778da49 100644 --- a/apps/edr-freight-web/backoffice/src/types/overview.ts +++ b/apps/edr-freight-web/backoffice/src/types/overview.ts @@ -10,6 +10,10 @@ export type { IOverviewTrendPoint, IOverviewDirectionTrendPoint, IOverviewTonnagePoint, + IOverviewPeriodTotals, + IOverviewRevenueSlice, + IOverviewRevenueFlow, + IOverviewHeatmapCell, IOverviewStatusCount, IOverviewPipelineCount, IOverviewPaymentTrendPoint,