-
-
{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" : ""}
-
-
-
+ setShowAdvanced((v) => !v)}
+ />
+ {activeFilterCount > 0 ? (
+ }
+ onClick={clearFilters}
+ >
+ Clear filters ({activeFilterCount})
+
+ ) : null}
+
+
+
-
-
- {activeFilterCount > 0 ? (
- }
- onClick={clearFilters}
- >
- Clear filters ({activeFilterCount})
-
- ) : null}
+
diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceListPage.tsx
index b0e199d12..2476bc24e 100644
--- a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceListPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceListPage.tsx
@@ -471,9 +471,6 @@ export default function DocumentClearanceListPage({
style={{ flex: 1, minWidth: 220 }}
/>
-
- {total} record{total !== 1 ? "s" : ""}
-
diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/WagonCancellationsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/WagonCancellationsPage.tsx
index b1b4ef3ca..091a8d5a2 100644
--- a/apps/edr-freight-web/backoffice/src/pages/bookings/WagonCancellationsPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/bookings/WagonCancellationsPage.tsx
@@ -24,6 +24,7 @@ import { api } from "@/auth/http";
import { useAuth } from "@/auth/useAuth";
import { PageContainer, PageHeader } from "@/components/page";
import { toDayString } from "@/hooks/useListControls";
+import { formatDate, formatMoney } from "@/lib/format";
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
import {
DataTable,
@@ -96,24 +97,6 @@ function StatusChip({ status }: { status: WagonCancellationStatus }) {
);
}
-function formatDate(iso: string | null | undefined): string {
- if (!iso) return "—";
- const d = new Date(iso);
- return Number.isNaN(d.getTime())
- ? "—"
- : d.toLocaleDateString(undefined, {
- year: "numeric",
- month: "short",
- day: "numeric",
- });
-}
-
-function formatAmount(amount: number, currency: string): string {
- return `${currency} ${Number(amount).toLocaleString(undefined, {
- minimumFractionDigits: 2,
- })}`;
-}
-
/**
* Staff view of partial wagon cancellations: every slice of capacity a
* customer gave back, its cancellation fee, and where the credit went
@@ -196,7 +179,9 @@ export default function WagonCancellationsPage() {
id: "company",
header: () =>
Company ,
cell: ({ row }) => (
-
{row.original.booking?.company?.name ?? "—"}
+
+ {row.original.booking?.company?.name ?? "—"}
+
),
},
{
@@ -209,7 +194,7 @@ export default function WagonCancellationsPage() {
header: () =>
Fee ,
cell: ({ row }) => (
- {formatAmount(row.original.feeAmount, row.original.feeCurrency)}
+ {formatMoney(row.original.feeAmount, row.original.feeCurrency, 2)}
),
},
@@ -218,7 +203,7 @@ export default function WagonCancellationsPage() {
header: () =>
Credit ,
cell: ({ row }) => (
- {formatAmount(row.original.creditAmount, row.original.feeCurrency)}
+ {formatMoney(row.original.creditAmount, row.original.feeCurrency, 2)}
),
},
@@ -370,7 +355,7 @@ export default function WagonCancellationsPage() {
{voiding.booking?.reference ?? voiding.bookingId} ·{" "}
{voiding.wagonsCancelled} wagon(s) · fee{" "}
- {formatAmount(voiding.feeAmount, voiding.feeCurrency)}
+ {formatMoney(voiding.feeAmount, voiding.feeCurrency, 2)}
The pending fee is dropped and the wagons stay on the booking.
diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx
index 197e947a5..029c53dd2 100644
--- a/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx
@@ -305,9 +305,6 @@ export default function ClearanceDocumentsPage() {
w={220}
aria-label="Filter by status"
/>
-
- {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") ? (
+
{
+ e.stopPropagation();
+ navigate(action.to(c.id));
+ }}
+ >
+ {action.label}
+
+ ) : (
+ 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 (
- {
- // Don't let the row-click navigation fire as well.
- e.stopPropagation();
- navigate(action.to(row.original.id));
- }}
- >
- {action.label}
-
- );
- },
- },
];
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"
/>
+ {
+ setKindFilter(v);
+ resetPage();
+ }}
+ clearable
+ radius="lg"
+ style={{ minWidth: 160 }}
+ aria-label="Filter by contract kind"
+ />
+ setShowAdvanced((v) => !v)}
+ />
+ {activeFilterCount > 0 ? (
+ }
+ onClick={clearFilters}
+ >
+ Clear filters ({activeFilterCount})
+
+ ) : null}
+
+
+
- {
- setKindFilter(v);
- resetPage();
- }}
- clearable
- radius="lg"
- style={{ minWidth: 160 }}
- aria-label="Filter by contract kind"
- />
- {activeFilterCount > 0 ? (
- }
- onClick={clearFilters}
- >
- Clear filters ({activeFilterCount})
-
- ) : 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() {