feat(freight-backoffice): support DJF in booking, contract and warehouse screens

Currency dropdowns/pickers (AdditionalPaymentsTab, ClearanceChargesTab,
PhasedClearanceActionPanel, AdviseDutyCard, ContractRequestsPage,
ruleEngine/resources, WarehouseRulesPage, VehicleDetailPage,
FeePreviewModal) offer DJF alongside ETB/USD; GlCreateBookingForm's
currency selector gets allowDjf next to allowUsd. Narrow 'ETB'|'USD'
type unions widened to include 'DJF' across the warehouse
billingCurrency plumbing (useWarehouses, warehouse.service, api.ts)
and the customer/invoice types.

Ad-hoc money() formatters (BookingTrucksPanel, AccrualDashboard,
ImportTrucksPage, EmptyReturnRequestsPage) and formatMoney call sites
that hardcoded 2 decimals (wagon-cancellation cards, BookingRequestDetailPage,
WagonCancellationsPage, PaymentsPage, WarehouseInvoicesPage) now use
currencyDecimals() from @edr/ui-common so DJF renders with 0 decimals
instead of forced cents. The 3 duplicate overview formatCurrency/
formatAmount helpers (typed 'ETB'|'USD') widen to accept any currency.

Two correctness fixes: OverviewRecentBookingsTable's currency==='USD'
? 'USD' : 'ETB' was mislabeling every non-USD currency as ETB; and
WarehouseInvoicesPage's gateway-method default now routes any
non-ETB currency (not just USD) to WAAFI, so DJF invoices get a
working default instead of TELEBIRR (ETB-only).

Claude-Session: https://claude.ai/code/session_01CZy77vCWhka3pnmVF9NDkL
This commit is contained in:
ghost2023
2026-09-04 11:53:56 +03:00
parent 34c49e4c50
commit 214f96dbae
30 changed files with 61 additions and 47 deletions

View File

@@ -72,6 +72,7 @@ import { AdditionalPaymentsTab } from "@/components/bookings/AdditionalPaymentsT
import { getStatusMeta } from "@/features/bookings/booking-status.config";
import { toBookingListRow } from "@/features/bookings/mapBookingListRow";
import { formatDateTime, formatMoney } from "@/lib/format";
import { currencyDecimals } from "@edr/ui-common";
import { cargoTonsAndItems } from "@/utils/cargoWeight";
import type { BookingDetail } from "@/types/booking";
import {
@@ -254,7 +255,7 @@ export default function BookingRequestDetailPage() {
const kpis: KpiItem[] = [
{
label: "Total value",
value: formatMoney(amount, booking.paymentCurrency, 2),
value: formatMoney(amount, booking.paymentCurrency, currencyDecimals(booking.paymentCurrency)),
hint: booking.paymentStatus,
icon: Wallet,
color: "edr-green",

View File

@@ -25,6 +25,7 @@ import { useAuth } from "@/auth/useAuth";
import { PageContainer, PageHeader } from "@/components/page";
import { toDayString } from "@/hooks/useListControls";
import { formatDate, formatMoney } from "@/lib/format";
import { currencyDecimals } from "@edr/ui-common";
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
import {
DataTable,
@@ -166,7 +167,7 @@ export default function WagonCancellationsPage() {
header: () => <span>Fee</span>,
cell: ({ row }) => (
<Text size="sm" style={{ fontVariantNumeric: "tabular-nums" }}>
{formatMoney(row.original.feeAmount, row.original.feeCurrency, 2)}
{formatMoney(row.original.feeAmount, row.original.feeCurrency, currencyDecimals(row.original.feeCurrency))}
</Text>
),
},
@@ -175,7 +176,7 @@ export default function WagonCancellationsPage() {
header: () => <span>Credit</span>,
cell: ({ row }) => (
<Text size="sm" style={{ fontVariantNumeric: "tabular-nums" }}>
{formatMoney(row.original.creditAmount, row.original.feeCurrency, 2)}
{formatMoney(row.original.creditAmount, row.original.feeCurrency, currencyDecimals(row.original.feeCurrency))}
</Text>
),
},
@@ -347,7 +348,7 @@ export default function WagonCancellationsPage() {
<Text size="sm">
{voiding.booking?.reference ?? voiding.bookingId} ·{" "}
{voiding.wagonsCancelled} wagon(s) · fee{" "}
{formatMoney(voiding.feeAmount, voiding.feeCurrency, 2)}
{formatMoney(voiding.feeAmount, voiding.feeCurrency, currencyDecimals(voiding.feeCurrency))}
</Text>
<Text size="sm" c="dimmed">
The pending fee is dropped and the wagons stay on the booking.

View File

@@ -82,6 +82,7 @@ const CONTRACT_KIND_OPTIONS = [
const CURRENCY_OPTIONS = [
{ value: "ETB", label: "ETB" },
{ value: "USD", label: "USD" },
{ value: "DJF", label: "DJF" },
];
/** value = `${sortBy}:${sortOrder}` for the sort Select. */

View File

@@ -279,6 +279,7 @@ const OperationsTab = ({ vehicle }: { vehicle: Vehicle }) => {
<Group gap="lg" mt={6}>
<Radio value="ETB" label="ETB" />
<Radio value="USD" label="USD" />
<Radio value="DJF" label="DJF" />
</Group>
</Radio.Group>
</SimpleGrid>

View File

@@ -274,7 +274,7 @@ function ConfirmCell({
export default function UsdPaymentsPanel({
currency,
}: {
currency: "USD" | "ETB";
currency: "USD" | "ETB" | "DJF";
}) {
const navigate = useNavigate();
// Namespaced: the ETB and USD tabs share this panel and live on the same URL

View File

@@ -27,6 +27,7 @@ import { useQuery } from "@tanstack/react-query";
import { KpiStrip } from "@/components/page";
import { ExportButton } from "@/components/export/ExportButton";
import { formatDate, formatMoney } from "@/lib/format";
import { currencyDecimals } from "@edr/ui-common";
import { api } from "@/services/api";
import type { PaymentMethod, PaymentRow } from "@/services/payments.service";
import {
@@ -149,7 +150,7 @@ export default function PaymentsPanel() {
header: () => <span className={tableHeader}>Amount</span>,
cell: ({ row }) => (
<span className="font-mono text-sm font-semibold tabular-nums text-foreground">
{formatMoney(row.original.amount, row.original.currency, 2)}
{formatMoney(row.original.amount, row.original.currency, currencyDecimals(row.original.currency))}
</span>
),
},

View File

@@ -450,6 +450,7 @@ export const rateUnitOptions = (
const CURRENCIES = [
{ label: "ETB (Birr)", value: "ETB" },
{ label: "USD", value: "USD" },
{ label: "DJF", value: "DJF" },
];
const PRIORITY_CONFIG_TYPES = [

View File

@@ -16,7 +16,7 @@ import {
Text,
Textarea,
} from "@mantine/core";
import { DataTable, type ColumnDef } from "@edr/ui-common";
import { DataTable, type ColumnDef, currencyDecimals } from "@edr/ui-common";
import { useAuth } from "@/auth/useAuth";
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
@@ -45,7 +45,7 @@ const STATUS_META: Record<EmptyReturnRequestStatus, { label: string; color: stri
const money = (amount: number | null | undefined, currency: string | null | undefined) =>
amount == null
? "—"
: `${Number(amount).toLocaleString(undefined, { minimumFractionDigits: 2 })} ${currency ?? ""}`.trim();
: `${Number(amount).toLocaleString(undefined, { minimumFractionDigits: currencyDecimals(currency) })} ${currency ?? ""}`.trim();
/**
* The queue for customer-initiated empty container returns: a booking sold

View File

@@ -1,4 +1,5 @@
import { Fragment, useMemo, useState } from "react";
import { currencyDecimals } from "@edr/ui-common";
import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
import {
ActionIcon,
@@ -78,7 +79,7 @@ const TRUCK_COLUMNS = [
] as const;
const money = (amount: number, currency: string) =>
`${Number(amount).toLocaleString(undefined, { maximumFractionDigits: 2 })} ${currency === "ETB" ? "ETB" : currency}`;
`${Number(amount).toLocaleString(undefined, { maximumFractionDigits: currencyDecimals(currency) })} ${currency === "ETB" ? "ETB" : currency}`;
export interface BookingGroup {

View File

@@ -17,7 +17,7 @@ import {
} from '@mantine/core';
import { Ban, CreditCard, DoorOpen, Download, ExternalLink, Eye, Receipt } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { DataTable, type ColumnDef } from '@edr/ui-common';
import { DataTable, type ColumnDef, currencyDecimals } from '@edr/ui-common';
import { applyClientFilters, FilterBar, useFilters, type FilterDef } from '@/components/filters';
import { PageContainer, PageHeader } from '@/components/page';
@@ -50,7 +50,7 @@ const STATUS_COLOR: Record<WarehouseInvoiceStatus, string> = {
CANCELLED: 'gray',
};
const fmt = (n: number, c: string) => formatMoney(n, c, 2);
const fmt = (n: number, c: string) => formatMoney(n, c, currencyDecimals(c));
const fmtDate = (d?: string | null) => (d ? new Date(d).toLocaleDateString() : '—');
const INVOICE_FILTER_DEFS: FilterDef[] = [
@@ -204,7 +204,8 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
const canGateClear = inv?.status === 'PAID' && Boolean(inv.inventoryId);
useEffect(() => {
setGatewayMethod(inv?.currency === 'USD' ? 'WAAFI' : 'TELEBIRR');
// WAAFI settles USD and DJF; TELEBIRR is ETB-only.
setGatewayMethod(inv?.currency !== 'ETB' ? 'WAAFI' : 'TELEBIRR');
setPayerAccount('');
}, [inv?.id, inv?.currency]);

View File

@@ -69,6 +69,7 @@ const TRADE = [
const CURRENCIES = [
{ value: 'USD', label: 'USD - Dollar' },
{ value: 'ETB', label: 'ETB - Birr' },
{ value: 'DJF', label: 'DJF - Djibouti Franc' },
];
const clean = (s: string) => s.trim() || undefined;