diff --git a/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts b/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts index b5341b0a1..0a0fed154 100644 --- a/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts +++ b/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts @@ -539,8 +539,46 @@ export class LastMileService { } async remove(id: string): Promise { - await this.findById(id); + const existing = await this.findById(id); + + // Every vehicle this delivery holds — junction + legacy + container rows. + const assignments = await this.dataSource.manager.find(LastMileVehicleAssignment, { + where: { lastMileId: id }, + }); + const allocations = await this.dataSource.manager.find(LastMileContainerAllocation, { + where: { lastMileId: id }, + }); + const vehicleIds = [ + ...new Set( + [ + ...assignments.map((a) => a.vehicleId), + ...allocations.map((a) => a.vehicleId), + existing.vehicleId ?? null, + ].filter((v): v is string => Boolean(v)), + ), + ]; + await this.lastMileRepository.softDelete(id); + if (assignments.length) { + await this.dataSource.manager.softDelete(LastMileVehicleAssignment, { lastMileId: id }); + } + + // Free every vehicle no longer held by another active trip (releaseIfUnused + // ignores this now soft-deleted record) and audit the release. + if (vehicleIds.length) { + await this.vehiclesService.releaseIfUnused(vehicleIds); + const bookingRef = await this.resolveBookingRef(existing); + for (const vehicleId of vehicleIds) { + const info = await this.vehicleInfo(vehicleId); + await this.history.record({ + eventType: FleetEventType.MILE_VEHICLE_RELEASED, + vehicleId, + lastMileId: id, + driverId: info.driverId, + metadata: { mile: 'LAST', bookingRef, vehiclePlate: info.plate, driverName: info.driverName }, + }); + } + } } async allocateContainers( 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 3ba71148e..f44c8e3a3 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -497,6 +497,8 @@ const LastMilePage = () => { const [distanceRows, setDistanceRows] = useState>({}); const [invoiceOpen, setInvoiceOpen] = useState(false); const [invoiceRecord, setInvoiceRecord] = useState(null); + // Record pending invoice-generation confirmation (shows a summary first). + const [invoiceConfirm, setInvoiceConfirm] = useState(null); const [allocationOpen, setAllocationOpen] = useState(false); const [allocationContainers, setAllocationContainers] = useState([]); @@ -1234,11 +1236,7 @@ const LastMilePage = () => { } disabled={!(row.original.exactKm != null && row.original.exactKm > 0)} - onClick={() => - generateInvoiceMutation.mutate(row.original.id, { - onSuccess: () => openInvoice(row.original), - }) - } + onClick={() => setInvoiceConfirm(row.original)} > Generate Invoice @@ -1858,6 +1856,69 @@ const LastMilePage = () => { + {/* Generate Invoice — confirmation summary */} + setInvoiceConfirm(null)} + title={Generate Invoice} + radius="lg" + centered + > + {invoiceConfirm && ( + + + {bookingRef(invoiceConfirm)} + {customerName(invoiceConfirm)} + + + + {(invoiceConfirm.vehicleAssignments ?? []).map((a) => { + const v = a.vehicle; + const label = v ? [v.code, v.plateNumber].filter(Boolean).join(" · ") : a.vehicleId; + return ( + + + {label} + {a.containerNumber ? ` · ${a.containerNumber}` : ""} + + {a.distanceKm != null ? `${a.distanceKm} km` : "—"} + + ); + })} + + + + Total distance + {invoiceConfirm.exactKm ?? 0} km + + + Invoice amount + {formatPrice(invoiceConfirm.remainingPayment)} + + + This creates the delivery-fee invoice. Confirm the distances and amount are correct. + + + + + + + )} + + {/* Container Allocation modal */}