From 214f96dbae8f1edb75dcc944f5deca2480f1642f Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Fri, 4 Sep 2026 11:53:56 +0300 Subject: [PATCH] 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 --- .../src/components/bookings/AdditionalPaymentsTab.tsx | 2 +- .../src/components/bookings/detail/BookingTrucksPanel.tsx | 5 +++-- .../wagon-cancellation/RebookWagonCancellationModal.tsx | 4 ++-- .../wagon-cancellation/WagonCancellationCreditCard.tsx | 5 +++-- .../src/components/contracts/ClearanceChargesTab.tsx | 2 +- .../src/components/contracts/GlCreateBookingForm.tsx | 7 ++++--- .../components/contracts/PhasedClearanceActionPanel.tsx | 6 +++--- .../src/components/contracts/gl-actions/AdviseDutyCard.tsx | 2 +- .../src/components/overview/OverviewPaymentChart.tsx | 2 +- .../components/overview/OverviewRecentBookingsTable.tsx | 3 +-- .../src/components/overview/summary/OverviewHeroKpis.tsx | 4 ++-- .../components/overview/tabs/OverviewBillingTabPanel.tsx | 2 +- .../src/components/warehouses/AccrualDashboard.tsx | 4 +++- .../src/components/warehouses/FeePreviewModal.tsx | 5 +++-- apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts | 6 +++--- .../src/pages/bookings/BookingRequestDetailPage.tsx | 3 ++- .../src/pages/bookings/WagonCancellationsPage.tsx | 7 ++++--- .../src/pages/contracts/ContractRequestsPage.tsx | 1 + .../backoffice/src/pages/fleet/VehicleDetailPage.tsx | 1 + .../backoffice/src/pages/invoices/UsdPaymentsPage.tsx | 2 +- .../backoffice/src/pages/payments/PaymentsPage.tsx | 3 ++- .../backoffice/src/pages/ruleEngine/config/resources.ts | 1 + .../src/pages/warehouses/EmptyReturnRequestsPage.tsx | 4 ++-- .../backoffice/src/pages/warehouses/ImportTrucksPage.tsx | 3 ++- .../src/pages/warehouses/WarehouseInvoicesPage.tsx | 7 ++++--- .../backoffice/src/pages/warehouses/WarehouseRulesPage.tsx | 1 + apps/edr-freight-web/backoffice/src/services/api.ts | 4 ++-- .../backoffice/src/services/warehouse.service.ts | 6 +++--- apps/edr-freight-web/backoffice/src/types/customer.ts | 4 ++-- apps/edr-freight-web/backoffice/src/types/invoice.ts | 2 +- 30 files changed, 61 insertions(+), 47 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/AdditionalPaymentsTab.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/AdditionalPaymentsTab.tsx index e6eb81a02..50ef7c4be 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/AdditionalPaymentsTab.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/AdditionalPaymentsTab.tsx @@ -36,7 +36,7 @@ import { downloadBookingFile, fetchViewableFile } from "@/services/files.service import { formatDate, formatDateTime } from "@/lib/format"; import { extractErrorMessage } from "@/utils/errorExtractor"; -const CURRENCIES = ["ETB", "USD"]; +const CURRENCIES = ["ETB", "USD", "DJF"]; const STATUS_META: Record = { DRAFT: { label: "Draft", color: "gray" }, diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingTrucksPanel.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingTrucksPanel.tsx index 65c9719ed..678913f78 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingTrucksPanel.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingTrucksPanel.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from "react"; import { useQueries, useQuery } from "@tanstack/react-query"; import { Button, Center, Group, Loader, SimpleGrid, Stack, Table, Text } from "@mantine/core"; import { Coins, Truck } from "lucide-react"; +import { currencyDecimals } from "@edr/ui-common"; import { api } from "@/services/api"; import { FeePreviewModal } from "@/components/warehouses/FeePreviewModal"; @@ -12,8 +13,8 @@ import { MetricTile } from "./MetricTile"; const money = (amount: number, currency: string) => `${Number(amount).toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, + minimumFractionDigits: currencyDecimals(currency), + maximumFractionDigits: currencyDecimals(currency), })} ${currency === "ETB" ? "Birr (ETB)" : currency}`; /** diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/wagon-cancellation/RebookWagonCancellationModal.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/wagon-cancellation/RebookWagonCancellationModal.tsx index 318ce3f9b..a03c29bec 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/wagon-cancellation/RebookWagonCancellationModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/wagon-cancellation/RebookWagonCancellationModal.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { Button, Group, Modal, Select, Stack, Text, TextInput } from "@mantine/core"; import { useMutation, useQuery } from "@tanstack/react-query"; import toast from "react-hot-toast"; -import { OperationDatePicker } from "@edr/ui-common"; +import { OperationDatePicker, currencyDecimals } from "@edr/ui-common"; import { api } from "@/auth/http"; import { api as rpc } from "@/services/api"; @@ -223,7 +223,7 @@ export function RebookWagonCancellationModal({ {cancellation.booking?.reference ?? cancellation.bookingId} ·{" "} {cancellation.wagonsCancelled} wagon(s) · credit{" "} - {formatMoney(cancellation.creditAmount, cancellation.feeCurrency, 2)} + {formatMoney(cancellation.creditAmount, cancellation.feeCurrency, currencyDecimals(cancellation.feeCurrency))} Shipment day diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/wagon-cancellation/WagonCancellationCreditCard.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/wagon-cancellation/WagonCancellationCreditCard.tsx index 4bca7cced..4f50c5921 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/wagon-cancellation/WagonCancellationCreditCard.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/wagon-cancellation/WagonCancellationCreditCard.tsx @@ -8,6 +8,7 @@ import { api } from "@/auth/http"; import { useAuth } from "@/auth/useAuth"; import { SectionCard } from "@/components/bookings/detail/SectionCard"; import { formatDate, formatMoney } from "@/lib/format"; +import { currencyDecimals } from "@edr/ui-common"; import { RebookWagonCancellationModal } from "./RebookWagonCancellationModal"; import { canRebookWagonCancellations, @@ -73,7 +74,7 @@ export function WagonCancellationCreditCard({ {Number(r.wagonsCancelled)} wagon(s) · credit{" "} - {formatMoney(Number(r.creditAmount), r.feeCurrency, 2)} + {formatMoney(Number(r.creditAmount), r.feeCurrency, currencyDecimals(r.feeCurrency))} {chip.label} @@ -83,7 +84,7 @@ export function WagonCancellationCreditCard({ Cancelled {formatDate(r.createdAt)} {r.fault ? ` · ${r.fault === "EDR" ? "EDR fault (no fee)" : "customer fault"}` : ""} {Number(r.feeAmount) > 0 - ? ` · fee ${formatMoney(Number(r.feeAmount), r.feeCurrency, 2)}${ + ? ` · fee ${formatMoney(Number(r.feeAmount), r.feeCurrency, currencyDecimals(r.feeCurrency))}${ r.feePaidAt ? " paid" : " unpaid" }` : ""} diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/ClearanceChargesTab.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/ClearanceChargesTab.tsx index 047d1361f..fb47ace60 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/ClearanceChargesTab.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/ClearanceChargesTab.tsx @@ -39,7 +39,7 @@ import { import { formatDateTime } from "@/lib/format"; import { extractErrorMessage } from "@/utils/errorExtractor"; -const CURRENCIES = ["ETB", "USD"]; +const CURRENCIES = ["ETB", "USD", "DJF"]; const STATUS_META: Record< Freight.ClearanceChargeStatus, diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx index 0d5a7046a..c607fcc3a 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx @@ -345,7 +345,7 @@ export default function GlCreateBookingForm() { const [notes, setNotes] = useState(""); // IMPORT bookings pick ETB or USD — starts empty so the choice is // deliberate (required before pricing). Everything else is forced to ETB. - const [paymentCurrency, setPaymentCurrency] = useState<"USD" | "ETB" | "">(""); + const [paymentCurrency, setPaymentCurrency] = useState<"USD" | "ETB" | "DJF" | "">(""); // What the containers carry — captured per booking (moved off the contract). const [cargoDescription, setCargoDescription] = useState(""); const [containerLines, setContainerLines] = useState([]); @@ -1144,7 +1144,7 @@ export default function GlCreateBookingForm() { ]); // Only IMPORT actually chooses — the rest bill ETB regardless of the state. - const effectiveCurrency: "USD" | "ETB" = + const effectiveCurrency: "USD" | "ETB" | "DJF" = isImport && paymentCurrency ? paymentCurrency : "ETB"; const currencyError = isImport && !paymentCurrency @@ -2351,7 +2351,7 @@ export default function GlCreateBookingForm() { {requestCurrencyLocked ? "The customer chose the billing currency on the shipment request — it cannot be changed." : isImport - ? "Import shipments may be invoiced in ETB or USD. USD is paid by bank transfer, not online." + ? "Import shipments may be invoiced in ETB, USD or DJF. USD is paid by bank transfer, not online." : "Shipments are invoiced in ETB."} diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/PhasedClearanceActionPanel.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/PhasedClearanceActionPanel.tsx index 313645db3..ea4ec0cb3 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/PhasedClearanceActionPanel.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/PhasedClearanceActionPanel.tsx @@ -1554,7 +1554,7 @@ function SecondDutyStep({ /> setCurrency(v ?? "ETB")} size="sm" @@ -2063,7 +2063,7 @@ function DutyStep({ /> setCurrency(v ?? "ETB")} size="sm" diff --git a/apps/edr-freight-web/backoffice/src/components/overview/OverviewPaymentChart.tsx b/apps/edr-freight-web/backoffice/src/components/overview/OverviewPaymentChart.tsx index f1a74ac4e..aa5857840 100644 --- a/apps/edr-freight-web/backoffice/src/components/overview/OverviewPaymentChart.tsx +++ b/apps/edr-freight-web/backoffice/src/components/overview/OverviewPaymentChart.tsx @@ -18,7 +18,7 @@ function formatDateLabel(date: string) { return parsed.toLocaleDateString(undefined, { month: "short", day: "numeric" }); } -function formatAmount(value: number, currency: "ETB" | "USD") { +function formatAmount(value: number, currency: string) { return new Intl.NumberFormat("en-US", { style: "currency", currency, diff --git a/apps/edr-freight-web/backoffice/src/components/overview/OverviewRecentBookingsTable.tsx b/apps/edr-freight-web/backoffice/src/components/overview/OverviewRecentBookingsTable.tsx index e1ceaec1c..3464141fe 100644 --- a/apps/edr-freight-web/backoffice/src/components/overview/OverviewRecentBookingsTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/overview/OverviewRecentBookingsTable.tsx @@ -9,10 +9,9 @@ import { SummaryCard } from "./summary/SummaryCard"; function formatAmount(amount: number | null, currency: string | null) { if (amount == null) return "—"; - const code = currency === "USD" ? "USD" : "ETB"; return new Intl.NumberFormat("en-US", { style: "currency", - currency: code, + currency: currency || "ETB", maximumFractionDigits: 0, }).format(amount); } 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 e076b1f18..307d9fc22 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 @@ -4,7 +4,7 @@ import { KpiStrip, type KpiItem } from "@/components/page"; import type { IOverviewKpis, IOverviewPeriodTotals } from "@/types/overview"; import { CountUp } from "./CountUp"; -function formatCurrency(amount: number, currency: "ETB" | "USD") { +function formatCurrency(amount: number, currency: string) { return new Intl.NumberFormat("en-US", { style: "currency", currency, @@ -13,7 +13,7 @@ function formatCurrency(amount: number, currency: "ETB" | "USD") { } /** Compact form ("ETB 58.6M") — the hero cell is too narrow for nine digits. */ -function formatCompactCurrency(amount: number, currency: "ETB" | "USD") { +function formatCompactCurrency(amount: number, currency: string) { return new Intl.NumberFormat("en-US", { style: "currency", currency, diff --git a/apps/edr-freight-web/backoffice/src/components/overview/tabs/OverviewBillingTabPanel.tsx b/apps/edr-freight-web/backoffice/src/components/overview/tabs/OverviewBillingTabPanel.tsx index 52b8bf161..c7e2a1fe5 100644 --- a/apps/edr-freight-web/backoffice/src/components/overview/tabs/OverviewBillingTabPanel.tsx +++ b/apps/edr-freight-web/backoffice/src/components/overview/tabs/OverviewBillingTabPanel.tsx @@ -18,7 +18,7 @@ import { OverviewKpiStrip } from "../OverviewKpiStrip"; import { OverviewPaymentChart } from "../OverviewPaymentChart"; import { overviewChartColors } from "../overview.styles"; -function formatCurrency(amount: number, currency: "ETB" | "USD") { +function formatCurrency(amount: number, currency: string) { return new Intl.NumberFormat("en-US", { style: "currency", currency, diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/AccrualDashboard.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/AccrualDashboard.tsx index 5170b345d..635a78800 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/AccrualDashboard.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/AccrualDashboard.tsx @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import { ActionIcon, Badge, Card, Group, Loader, Menu, SimpleGrid, Stack, Table, Text, ThemeIcon } from '@mantine/core'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { AlertTriangle, Bell, BellOff, Check, Clock, DollarSign, MoreVertical } from 'lucide-react'; +import { currencyDecimals } from '@edr/ui-common'; import { useAccrualDashboard } from '@/hooks/useWarehouses'; import { warehouseService } from '@/services/warehouse.service'; @@ -15,7 +16,8 @@ const ALERT_META: Record = { }; function money(amount: number, currency: string): string { - return `${amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${currency}`; + const decimals = currencyDecimals(currency); + return `${amount.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })} ${currency}`; } function freeDaysLabel(row: AccrualDashboardRow): string { diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/FeePreviewModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/FeePreviewModal.tsx index cb2cbc658..058fc96bd 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/FeePreviewModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/FeePreviewModal.tsx @@ -113,7 +113,7 @@ function Row({ label, value }: { label: string; value: string }) { /** Batch 5 fee preview + Batch 6 invoice generation / gate clearance for an inventory item. */ export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModalProps) { const { toast } = useToast(); - const [billingCurrency, setBillingCurrency] = useState<'ETB' | 'USD'>('USD'); + const [billingCurrency, setBillingCurrency] = useState<'ETB' | 'USD' | 'DJF'>('USD'); const enabledId = opened ? inventoryId ?? undefined : undefined; const { data, isLoading } = useQuery( api.warehouses.feePreview.queryOptions({ @@ -211,10 +211,11 @@ export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModa setBillingCurrency(value as 'ETB' | 'USD')} + onChange={(value) => setBillingCurrency(value as 'ETB' | 'USD' | 'DJF')} data={[ { value: 'USD', label: 'USD' }, { value: 'ETB', label: 'Birr' }, + { value: 'DJF', label: 'DJF' }, ]} disabled={Boolean(activeInvoice)} /> diff --git a/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts b/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts index 6ed13636a..a6db563cd 100644 --- a/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts +++ b/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts @@ -221,7 +221,7 @@ export function useOnTimeDispatch() { } /** Live per-item fee accrual (storage/demurrage) with alerts. */ -export function useAccrualDashboard(billingCurrency?: 'ETB' | 'USD') { +export function useAccrualDashboard(billingCurrency?: 'ETB' | 'USD' | 'DJF') { return useQuery({ queryKey: ['warehouse-fees', 'accrual-dashboard', billingCurrency ?? 'USD'], queryFn: () => warehouseService.accrualDashboard(billingCurrency).then((r) => r.data), @@ -598,7 +598,7 @@ export const useUpdateFeeRule = () => export const useDeleteFeeRule = () => useRuleMutation((id: string) => warehouseService.deleteFeeRule(id), ['warehouse-fee-rules']); -export function useFeePreview(inventoryId?: string, billingCurrency: 'ETB' | 'USD' = 'USD') { +export function useFeePreview(inventoryId?: string, billingCurrency: 'ETB' | 'USD' | 'DJF' = 'USD') { return useQuery({ queryKey: ['warehouse-inventory', inventoryId, 'fee-preview', billingCurrency], queryFn: () => warehouseService.feePreview(inventoryId as string, billingCurrency).then((r) => r.data), @@ -649,7 +649,7 @@ export function useGenerateInvoice() { }: { inventoryId: string; confirmZero?: boolean; - billingCurrency?: 'ETB' | 'USD'; + billingCurrency?: 'ETB' | 'USD' | 'DJF'; }) => warehouseService.generateInvoice(inventoryId, confirmZero, billingCurrency).then((r) => r.data), onSuccess, }); 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 a75e242e1..579c8f290 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx @@ -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", 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 1a18d4cf0..7d1685328 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/WagonCancellationsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/WagonCancellationsPage.tsx @@ -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: () => Fee, cell: ({ row }) => ( - {formatMoney(row.original.feeAmount, row.original.feeCurrency, 2)} + {formatMoney(row.original.feeAmount, row.original.feeCurrency, currencyDecimals(row.original.feeCurrency))} ), }, @@ -175,7 +176,7 @@ export default function WagonCancellationsPage() { header: () => Credit, cell: ({ row }) => ( - {formatMoney(row.original.creditAmount, row.original.feeCurrency, 2)} + {formatMoney(row.original.creditAmount, row.original.feeCurrency, currencyDecimals(row.original.feeCurrency))} ), }, @@ -347,7 +348,7 @@ export default function WagonCancellationsPage() { {voiding.booking?.reference ?? voiding.bookingId} ·{" "} {voiding.wagonsCancelled} wagon(s) · fee{" "} - {formatMoney(voiding.feeAmount, voiding.feeCurrency, 2)} + {formatMoney(voiding.feeAmount, voiding.feeCurrency, currencyDecimals(voiding.feeCurrency))} The pending fee is dropped and the wagons stay on the booking. 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 455a6473e..b62d405fe 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx @@ -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. */ 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 bcfadb698..c0d577d89 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/VehicleDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/VehicleDetailPage.tsx @@ -279,6 +279,7 @@ const OperationsTab = ({ vehicle }: { vehicle: Vehicle }) => { + diff --git a/apps/edr-freight-web/backoffice/src/pages/invoices/UsdPaymentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/invoices/UsdPaymentsPage.tsx index 8b3e58a5f..eeceae91b 100644 --- a/apps/edr-freight-web/backoffice/src/pages/invoices/UsdPaymentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/invoices/UsdPaymentsPage.tsx @@ -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 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 2c30fb11f..868137ba3 100644 --- a/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx @@ -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: () => Amount, cell: ({ row }) => ( - {formatMoney(row.original.amount, row.original.currency, 2)} + {formatMoney(row.original.amount, row.original.currency, currencyDecimals(row.original.currency))} ), }, diff --git a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts index 62fecd600..0ec64f0bc 100644 --- a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts +++ b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts @@ -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 = [ diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/EmptyReturnRequestsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/EmptyReturnRequestsPage.tsx index 3c2b487df..41b4c8679 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/EmptyReturnRequestsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/EmptyReturnRequestsPage.tsx @@ -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 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 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 a074006d8..05e89a1f9 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/ImportTrucksPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/ImportTrucksPage.tsx @@ -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 { 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 974755a71..1112778dd 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx @@ -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 = { 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]); 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 8c432f92a..02ced15fb 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx @@ -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; diff --git a/apps/edr-freight-web/backoffice/src/services/api.ts b/apps/edr-freight-web/backoffice/src/services/api.ts index c130c12a5..8e48ead12 100644 --- a/apps/edr-freight-web/backoffice/src/services/api.ts +++ b/apps/edr-freight-web/backoffice/src/services/api.ts @@ -1532,7 +1532,7 @@ export const api = { ), feePreview: endpoint< - { inventoryId: string; billingCurrency?: "ETB" | "USD" }, + { inventoryId: string; billingCurrency?: "ETB" | "USD" | "DJF" }, FeePreview[] >( "warehouse-inventory", @@ -1884,7 +1884,7 @@ export const api = { { inventoryId: string; confirmZero?: boolean; - billingCurrency?: "ETB" | "USD"; + billingCurrency?: "ETB" | "USD" | "DJF"; }, WarehouseFeeInvoice >( diff --git a/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts b/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts index 98a8d59e4..18368110f 100644 --- a/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts @@ -558,11 +558,11 @@ export const warehouseService = { updateFeeRule: (id: string, payload: Partial) => apiClient.patch(URL_CONSTANTS.WAREHOUSE_RULES.FEES_BY_ID(id), payload), deleteFeeRule: (id: string) => apiClient.delete(URL_CONSTANTS.WAREHOUSE_RULES.FEES_BY_ID(id)), - feePreview: (inventoryId: string, billingCurrency?: 'ETB' | 'USD') => + feePreview: (inventoryId: string, billingCurrency?: 'ETB' | 'USD' | 'DJF') => apiClient.get(URL_CONSTANTS.WAREHOUSE_RULES.FEE_PREVIEW(inventoryId), { params: cleanParams({ billingCurrency }), }), - accrualDashboard: (billingCurrency?: 'ETB' | 'USD') => + accrualDashboard: (billingCurrency?: 'ETB' | 'USD' | 'DJF') => apiClient.get(URL_CONSTANTS.WAREHOUSE_RULES.ACCRUAL_DASHBOARD, { params: cleanParams({ billingCurrency }), }), @@ -592,7 +592,7 @@ export const warehouseService = { apiClient.get(URL_CONSTANTS.WAREHOUSE_INVOICES.FOR_INVENTORY(inventoryId)), invoicesForBooking: (bookingId: string) => apiClient.get(URL_CONSTANTS.WAREHOUSE_INVOICES.FOR_BOOKING(bookingId)), - generateInvoice: (inventoryId: string, confirmZero = false, billingCurrency?: 'ETB' | 'USD') => + generateInvoice: (inventoryId: string, confirmZero = false, billingCurrency?: 'ETB' | 'USD' | 'DJF') => apiClient.post(URL_CONSTANTS.WAREHOUSE_INVOICES.GENERATE(inventoryId), { confirmZero, billingCurrency, diff --git a/apps/edr-freight-web/backoffice/src/types/customer.ts b/apps/edr-freight-web/backoffice/src/types/customer.ts index 1d3eb7588..57be90f90 100644 --- a/apps/edr-freight-web/backoffice/src/types/customer.ts +++ b/apps/edr-freight-web/backoffice/src/types/customer.ts @@ -420,7 +420,7 @@ export interface CustomerBooking { originLabel: string; destinationLabel: string; totalAmount: number; - currency: "ETB" | "USD"; + currency: "ETB" | "USD" | "DJF"; scheduledDate?: string | null; createdAt: string; } @@ -469,7 +469,7 @@ export interface CustomerPayment { /** Booking reference the payment settles. */ bookingReference: string; amount: number; - currency: "ETB" | "USD"; + currency: "ETB" | "USD" | "DJF"; method: CustomerPaymentMethod; status: CustomerPaymentStatus; paidAt?: string | null; diff --git a/apps/edr-freight-web/backoffice/src/types/invoice.ts b/apps/edr-freight-web/backoffice/src/types/invoice.ts index 218e478fa..0807894c1 100644 --- a/apps/edr-freight-web/backoffice/src/types/invoice.ts +++ b/apps/edr-freight-web/backoffice/src/types/invoice.ts @@ -77,7 +77,7 @@ export interface InvoiceListFilter { /** CSV of normalised UPPER_SNAKE payment methods (see `PAYMENT_METHOD_OPTIONS`). */ paymentMethods?: string; search?: string; - currency?: "USD" | "ETB"; + currency?: "USD" | "ETB" | "DJF"; /** ISO instants — inclusive bounds on `issuedAt` / `dueAt`. */ issuedFrom?: string; issuedTo?: string;