mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 17:45:42 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user