This commit is contained in:
natib21
2026-07-03 20:51:32 +00:00
parent 4ddf03e357
commit 90ab1bd042
2 changed files with 105 additions and 6 deletions

View File

@@ -539,8 +539,46 @@ export class LastMileService {
}
async remove(id: string): Promise<void> {
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(

View File

@@ -497,6 +497,8 @@ const LastMilePage = () => {
const [distanceRows, setDistanceRows] = useState<Record<string, string>>({});
const [invoiceOpen, setInvoiceOpen] = useState(false);
const [invoiceRecord, setInvoiceRecord] = useState<LastMileRecord | null>(null);
// Record pending invoice-generation confirmation (shows a summary first).
const [invoiceConfirm, setInvoiceConfirm] = useState<LastMileRecord | null>(null);
const [allocationOpen, setAllocationOpen] = useState(false);
const [allocationContainers, setAllocationContainers] = useState<LastMileContainerRow[]>([]);
@@ -1234,11 +1236,7 @@ const LastMilePage = () => {
<Menu.Item
leftSection={<Receipt size={15} />}
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
</Menu.Item>
@@ -1858,6 +1856,69 @@ const LastMilePage = () => {
</Stack>
</Modal>
{/* Generate Invoice — confirmation summary */}
<Modal
opened={Boolean(invoiceConfirm)}
onClose={() => setInvoiceConfirm(null)}
title={<Text fw={600}>Generate Invoice</Text>}
radius="lg"
centered
>
{invoiceConfirm && (
<Stack gap="md">
<Group justify="space-between">
<Text fw={600} size="sm">{bookingRef(invoiceConfirm)}</Text>
<Text size="sm" c="dimmed">{customerName(invoiceConfirm)}</Text>
</Group>
<Card withBorder padding="sm" radius="md" bg="var(--mantine-color-gray-0)">
<Stack gap={6}>
{(invoiceConfirm.vehicleAssignments ?? []).map((a) => {
const v = a.vehicle;
const label = v ? [v.code, v.plateNumber].filter(Boolean).join(" · ") : a.vehicleId;
return (
<Group key={a.id} justify="space-between" wrap="nowrap">
<Text size="sm">
{label}
{a.containerNumber ? ` · ${a.containerNumber}` : ""}
</Text>
<Text size="sm">{a.distanceKm != null ? `${a.distanceKm} km` : "—"}</Text>
</Group>
);
})}
</Stack>
</Card>
<Group justify="space-between">
<Text size="sm" c="dimmed">Total distance</Text>
<Text size="sm" fw={500}>{invoiceConfirm.exactKm ?? 0} km</Text>
</Group>
<Group justify="space-between">
<Text fw={600}>Invoice amount</Text>
<Text fw={700}>{formatPrice(invoiceConfirm.remainingPayment)}</Text>
</Group>
<Text size="xs" c="dimmed">
This creates the delivery-fee invoice. Confirm the distances and amount are correct.
</Text>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={() => setInvoiceConfirm(null)}>Cancel</Button>
<Button
loading={generateInvoiceMutation.isPending}
onClick={() =>
generateInvoiceMutation.mutate(invoiceConfirm.id, {
onSuccess: () => {
const rec = invoiceConfirm;
setInvoiceConfirm(null);
openInvoice(rec);
},
})
}
>
Generate Invoice
</Button>
</Group>
</Stack>
)}
</Modal>
{/* Container Allocation modal */}
<Modal
opened={allocationOpen}