This commit is contained in:
natib21
2026-07-04 01:31:08 +00:00
parent 92eabfc94b
commit 58c49d34b2
4 changed files with 51 additions and 20 deletions

View File

@@ -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 && <InfoRow label="Pickup location" value={pickupLocation(record)} />}
<InfoRow label="Destination (origin yard)" value={destinationYardName(record)} />
<InfoRow label="Cargo" value={cargoDesc(record)} />
<InfoRow label="Advanced Payment" value={formatPrice(record.advancedPayment)} />
<InfoRow label="Post Payment" value={formatPrice(record.remainingPayment)} />
<InfoRow label="Advanced Payment" value={formatPrice(record.advancedPayment, currencyOf(record))} />
<InfoRow label="Post Payment" value={formatPrice(record.remainingPayment, currencyOf(record))} />
<InfoRow label="Contact" value={contactPersonName(record)} />
<InfoRow label="Phone" value={contactPhone(record)} />
<InfoRow label="Requested date" value={requestedDate(record)} />
@@ -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 = () => {
</Menu.Item>
<Menu.Item
leftSection={<RefreshCw size={15} />}
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})
</Button>
</Group>
</Stack>

View File

@@ -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 }) => {
<InfoRow label="Pickup (origin yard)" value={originYardName(record)} />
{hasDeliveryAddress && <InfoRow label="Destination" value={deliveryLocation(record)} />}
<InfoRow label="Cargo" value={cargoDesc(record)} />
<InfoRow label="Advanced Payment" value={formatPrice(record.advancedPayment)} />
<InfoRow label="Post Payment" value={formatPrice(record.remainingPayment)} />
<InfoRow label="Advanced Payment" value={formatPrice(record.advancedPayment, currencyOf(record))} />
<InfoRow label="Post Payment" value={formatPrice(record.remainingPayment, currencyOf(record))} />
<InfoRow label="Contact" value={contactPersonName(record)} />
<InfoRow label="Phone" value={contactPhone(record)} />
<InfoRow label="Requested date" value={requestedDate(record)} />
@@ -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 = () => {
</Menu.Item>
<Menu.Item
leftSection={<RefreshCw size={15} />}
disabled={!assigned || delivered}
disabled={!assigned || delivered || Boolean(row.original.invoice)}
onClick={() => openAssign(row.original.id)}
>
Reassign
@@ -1250,7 +1264,7 @@ const LastMilePage = () => {
<Menu.Item
color="red"
leftSection={<X size={15} />}
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})
</Button>
</Group>
</Stack>
@@ -1928,7 +1942,7 @@ const LastMilePage = () => {
</Group>
<Group justify="space-between">
<Text fw={600}>Invoice amount</Text>
<Text fw={700}>{formatPrice(invoiceConfirm.remainingPayment)}</Text>
<Text fw={700}>{formatPrice(invoiceConfirm.remainingPayment, currencyOf(invoiceConfirm))}</Text>
</Group>
<Text size="xs" c="dimmed">
This creates the delivery-fee invoice. Confirm the distances and amount are correct.

View File

@@ -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;

View File

@@ -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;