mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
mile
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user