mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge pull request #444 from Tria-plc/freight/feature/first_mile_invoice
fix
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user