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 2da6e5bb8..f7a4b82bc 100644
--- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx
@@ -52,8 +52,8 @@ import { vehiclesService } from "@/services/vehicles.service";
import { ratesService } from "@/services/rates.service";
import type { BookingDetail } from "@/types/booking";
-const formatPrice = (amount: number) =>
- `ETB ${amount.toLocaleString("en-US", {
+const formatPrice = (amount: number | string | null | undefined, currency = "ETB") =>
+ `${currency || "ETB"} ${(Number(amount) || 0).toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
@@ -111,8 +111,17 @@ const vehicleLabel = (record: FirstMileRecord) => {
const isAssigned = (record: FirstMileRecord) => Boolean(record.vehicleId);
+// Paid = record flag set OR its invoice reached PAID.
+const isPaidRecord = (r: FirstMileRecord) =>
+ Boolean((r as { paid?: boolean }).paid) ||
+ (r.invoice?.status ?? "").toUpperCase() === "PAID";
+// Post payment pending = a post payment is owed but not yet paid.
+const isPostPaymentPending = (r: FirstMileRecord) =>
+ Number(r.remainingPayment) > 0 && !isPaidRecord(r);
+
// Map API record → display fields used in modals and trip slip
const bookingRef = (r: FirstMileRecord) => r.booking?.reference ?? r.bookingId;
+const currencyOf = (r: FirstMileRecord) => r.booking?.paymentCurrency ?? "ETB";
const customerName = (r: FirstMileRecord) => r.booking?.company?.name ?? "—";
const pickupLocation = (r: FirstMileRecord) => r.booking?.firstMilePickupAddress ?? "—";
const cargoDesc = (r: FirstMileRecord) => {
@@ -169,8 +178,8 @@ const BookingInfo = ({ record }: { record: FirstMileRecord }) => {
{hasPickupAddress && }
-
-
+
+
@@ -189,8 +198,8 @@ const tripSlipRows = (record: FirstMileRecord): [string, string][] => [
["Pickup location", pickupLocation(record)],
["Destination yard", destinationYardName(record)],
["Cargo", cargoDesc(record)],
- ["Advanced Payment", formatPrice(record.advancedPayment)],
- ["Post Payment", formatPrice(record.remainingPayment)],
+ ["Advanced Payment", formatPrice(record.advancedPayment, currencyOf(record))],
+ ["Post Payment", formatPrice(record.remainingPayment, currencyOf(record))],
["Vehicle", vehicleLabel(record) ?? "Unassigned"],
["Contact", `${contactPersonName(record)} · ${contactPhone(record)}`],
["Requested date", requestedDate(record)],
@@ -589,6 +598,7 @@ const FirstMilePage = () => {
};
const matchesFilter = (r: FirstMileRecord) => {
+ if (filterPostPaymentPending && !isPostPaymentPending(r)) return false;
switch (statusFilter) {
case "ALL": return true;
case "ASSIGNED": return isAssigned(r);
@@ -615,6 +625,11 @@ const FirstMilePage = () => {
return counts;
}, [records]);
+ const postPaymentPendingCount = useMemo(
+ () => records.filter(isPostPaymentPending).length,
+ [records],
+ );
+
const filteredRecords = useMemo(() => {
const term = search.trim().toLowerCase();
return records.filter((r) => {
@@ -751,7 +766,7 @@ const FirstMilePage = () => {
id: "postPayment",
header: "Post Payment",
meta: { headerClassName, cellClassName },
- cell: ({ row }) => formatPrice(row.original.remainingPayment),
+ cell: ({ row }) => formatPrice(row.original.remainingPayment, currencyOf(row.original)),
},
{
id: "vehicle",
@@ -853,7 +868,7 @@ const FirstMilePage = () => {
}
- disabled={!assigned}
+ disabled={!assigned || Boolean(row.original.invoice)}
onClick={() => openAssign(row.original.id)}
>
Reassign
@@ -975,7 +990,7 @@ const FirstMilePage = () => {
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
>
- Post Payment Pending
+ Post Payment Pending ({postPaymentPendingCount})
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 31b4a6cff..d7545fd7b 100644
--- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx
@@ -58,8 +58,8 @@ import { ratesService } from "@/services/rates.service";
import { ReleaseOrderModal, type ReleaseOrderTruckPrefill } from "@/components/warehouses/ReleaseOrderModal";
import { LastMileStepper, type LastMileStepState } from "@/components/operations/LastMileSteps";
-const formatPrice = (amount: number) =>
- `ETB ${amount.toLocaleString("en-US", {
+const formatPrice = (amount: number | string | null | undefined, currency = "ETB") =>
+ `${currency || "ETB"} ${(Number(amount) || 0).toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
@@ -156,6 +156,14 @@ const containerLabels = (record: LastMileRecord): string[] => {
const isAssigned = (record: LastMileRecord) => Boolean(record.vehicleId);
+// Paid = record flag set OR its invoice reached PAID.
+const isPaidRecord = (r: LastMileRecord) =>
+ Boolean((r as { paid?: boolean }).paid) ||
+ (r.invoice?.status ?? "").toUpperCase() === "PAID";
+// Post payment pending = a post payment is owed but not yet paid.
+const isPostPaymentPending = (r: LastMileRecord) =>
+ Number(r.remainingPayment) > 0 && !isPaidRecord(r);
+
const fmtStamp = (iso?: string | null) => {
if (!iso) return null;
const d = new Date(iso);
@@ -209,6 +217,7 @@ const computeLastMileSteps = (
};
const bookingRef = (r: LastMileRecord) => r.booking?.reference ?? r.bookingId;
+const currencyOf = (r: LastMileRecord) => r.booking?.paymentCurrency ?? "ETB";
const customerName = (r: LastMileRecord) => r.booking?.company?.name ?? "—";
const deliveryLocation = (r: LastMileRecord) => r.booking?.lastMileDeliveryAddress ?? "—";
const cargoDesc = (r: LastMileRecord) => {
@@ -311,8 +320,8 @@ const BookingInfo = ({ record }: { record: LastMileRecord }) => {
{hasDeliveryAddress && }
-
-
+
+
@@ -358,7 +367,7 @@ const tripSlipRows = (
["Pickup (origin yard)", originYardName(record)],
["Destination", deliveryLocation(record)],
["Cargo", cargoDesc(record)],
- ["Post Payment", formatPrice(record.remainingPayment)],
+ ["Post Payment", formatPrice(record.remainingPayment, currencyOf(record))],
...vehicleRows,
["Contact", `${contactPersonName(record)} · ${contactPhone(record)}`],
["Requested date", requestedDate(record)],
@@ -870,11 +879,16 @@ const LastMilePage = () => {
return counts;
}, [records]);
+ const postPaymentPendingCount = useMemo(
+ () => records.filter(isPostPaymentPending).length,
+ [records],
+ );
+
const filteredRecords = useMemo(() => {
const term = search.trim().toLowerCase();
return records.filter((r) => {
if (!matchesFilter(r)) return false;
- if (filterPostPaymentPending && !(r.remainingPayment > 0)) return false;
+ if (filterPostPaymentPending && !isPostPaymentPending(r)) return false;
if (!term) return true;
return [bookingRef(r), customerName(r), deliveryLocation(r), cargoDesc(r)]
.join(" ")
@@ -1089,7 +1103,7 @@ const LastMilePage = () => {
id: "postPayment",
header: "Post Payment",
meta: { headerClassName, cellClassName },
- cell: ({ row }) => formatPrice(row.original.remainingPayment),
+ cell: ({ row }) => formatPrice(row.original.remainingPayment, currencyOf(row.original)),
},
{
id: "vehicle",
@@ -1242,7 +1256,7 @@ const LastMilePage = () => {
}
- disabled={!assigned || delivered}
+ disabled={!assigned || delivered || Boolean(row.original.invoice)}
onClick={() => openAssign(row.original.id)}
>
Reassign
@@ -1250,7 +1264,7 @@ const LastMilePage = () => {
}
- disabled={!assigned || delivered}
+ disabled={!assigned || delivered || Boolean(row.original.invoice)}
onClick={() =>
setVehiclesMutation.mutate(
{ id: row.original.id, vehicles: [] },
@@ -1387,7 +1401,7 @@ const LastMilePage = () => {
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
>
- Post Payment Pending
+ Post Payment Pending ({postPaymentPendingCount})
@@ -1928,7 +1942,7 @@ const LastMilePage = () => {
Invoice amount
- {formatPrice(invoiceConfirm.remainingPayment)}
+ {formatPrice(invoiceConfirm.remainingPayment, currencyOf(invoiceConfirm))}
This creates the delivery-fee invoice. Confirm the distances and amount are correct.
diff --git a/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts b/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts
index 1418cb636..a1c2ec8e2 100644
--- a/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts
+++ b/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts
@@ -16,6 +16,7 @@ export interface FirstMileBooking {
cargoFreeText?: string | null;
cargoTotalWeightVgm: number;
totalAmount: number;
+ paymentCurrency?: string | null;
scheduledDate?: string | null;
company?: { id: string; name?: string; phone?: string | null; contactPersonName?: string | null; contactPersonPhone?: string | null } | null;
serviceType?: { id: string; label?: string } | null;
diff --git a/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts b/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts
index c68c5a9a2..2f8a1cec5 100644
--- a/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts
+++ b/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts
@@ -16,6 +16,7 @@ export interface LastMileBooking {
cargoFreeText?: string | null;
cargoTotalWeightVgm: number;
totalAmount: number;
+ paymentCurrency?: string | null;
scheduledDate?: string | null;
company?: { id: string; name?: string; phone?: string | null; contactPersonName?: string | null; contactPersonPhone?: string | null } | null;
serviceType?: { id: string; name?: string; label?: string } | null;