This commit is contained in:
natib21
2026-07-03 20:21:16 +00:00
parent 5d1a2aa319
commit 16bd7fabcc
7 changed files with 191 additions and 38 deletions

View File

@@ -492,7 +492,8 @@ const LastMilePage = () => {
const [arrivalSearch, setArrivalSearch] = useState("");
const [distanceOpen, setDistanceOpen] = useState(false);
const [distanceValue, setDistanceValue] = useState("");
// Per-vehicle actual distance, keyed by vehicleId.
const [distanceRows, setDistanceRows] = useState<Record<string, string>>({});
const [invoiceOpen, setInvoiceOpen] = useState(false);
const [invoiceRecord, setInvoiceRecord] = useState<LastMileRecord | null>(null);
@@ -593,14 +594,19 @@ const LastMilePage = () => {
},
});
const updateDistanceMutation = useMutation({
mutationFn: ({ id, exactKm, remainingPayment }: { id: string; exactKm: number; remainingPayment?: number }) =>
lastMileService.update(id, { exactKm, ...(remainingPayment != null && { remainingPayment }) }),
const distanceMutation = useMutation({
mutationFn: ({
id,
distances,
remainingPayment,
}: {
id: string;
distances: Array<{ vehicleId: string; distanceKm: number }>;
remainingPayment?: number;
}) => lastMileService.setDistances(id, distances, remainingPayment),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.list() });
if (activeRecord) {
toast({ title: "Distance updated", description: `${bookingRef(activeRecord)}${distanceValue} km` });
}
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT });
toast({ title: "Distances saved", description: activeRecord ? bookingRef(activeRecord) : undefined });
closeDistance();
},
onError: () => {
@@ -608,6 +614,17 @@ const LastMilePage = () => {
},
});
const generateInvoiceMutation = useMutation({
mutationFn: (id: string) => lastMileService.generateInvoice(id),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT });
toast({ title: "Invoice generated" });
},
onError: () => {
toast({ title: "Invoice generation failed", variant: "destructive" });
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => lastMileService.remove(id),
onSuccess: () => {
@@ -707,15 +724,20 @@ const LastMilePage = () => {
};
const openDistance = (id: string) => {
const rec = records.find((r) => r.id === id);
const rows: Record<string, string> = {};
for (const a of rec?.vehicleAssignments ?? []) {
rows[a.vehicleId] = a.distanceKm != null ? String(a.distanceKm) : "";
}
setActiveId(id);
setDistanceValue("");
setDistanceRows(rows);
setDistanceOpen(true);
};
const closeDistance = () => {
setDistanceOpen(false);
setActiveId(null);
setDistanceValue("");
setDistanceRows({});
};
const openInvoice = (record: LastMileRecord) => {
@@ -741,24 +763,27 @@ const LastMilePage = () => {
};
const handleSaveDistance = () => {
const distance = parseFloat(distanceValue);
if (!activeId || isNaN(distance) || distance < 0) {
toast({ title: "Invalid distance", description: "Enter a valid distance value.", variant: "destructive" });
const distances = Object.entries(distanceRows)
.map(([vehicleId, val]) => ({ vehicleId, distanceKm: parseFloat(val) }))
.filter((d) => !Number.isNaN(d.distanceKm) && d.distanceKm >= 0);
if (!activeId || !distances.length) {
toast({ title: "Invalid distance", description: "Enter a distance for at least one vehicle.", variant: "destructive" });
return;
}
const total = distances.reduce((s, d) => s + d.distanceKm, 0);
let remainingPayment: number | undefined;
if (ratesData?.data) {
const lastMileRate = ratesData.data.find(
(r) => r.rateType === "LAST_MILE" && (r.status === "LIVE" || r.status === "DRAFT")
);
if (lastMileRate) {
const rateValue = parseFloat(lastMileRate.rateValue);
remainingPayment = distance * rateValue;
remainingPayment = total * parseFloat(lastMileRate.rateValue);
}
}
updateDistanceMutation.mutate({ id: activeId, exactKm: distance, remainingPayment });
distanceMutation.mutate({ id: activeId, distances, remainingPayment });
};
const activeRecord = useMemo(
@@ -1206,7 +1231,11 @@ const LastMilePage = () => {
<Menu.Item
leftSection={<Receipt size={15} />}
disabled={!(row.original.exactKm != null && row.original.exactKm > 0)}
onClick={() => openInvoice(row.original)}
onClick={() =>
generateInvoiceMutation.mutate(row.original.id, {
onSuccess: () => openInvoice(row.original),
})
}
>
Generate Invoice
</Menu.Item>
@@ -1693,34 +1722,51 @@ const LastMilePage = () => {
<Stack gap="md">
{activeRecord && (
<Card withBorder padding="md" radius="md" bg="var(--mantine-color-gray-0)">
<Stack gap="sm">
<Group justify="space-between">
<Text fw={600} size="sm">{bookingRef(activeRecord)}</Text>
<Group justify="space-between">
<Text size="xs" c="dimmed">Customer</Text>
<Text size="sm">{customerName(activeRecord)}</Text>
</Group>
<Group justify="space-between">
<Text size="xs" c="dimmed">Est. Distance (KM)</Text>
<Text size="sm">{activeRecord.estimatedKm ?? "—"}</Text>
</Group>
</Stack>
<Text size="xs" c="dimmed">Est. {activeRecord.estimatedKm ?? "—"} km</Text>
</Group>
</Card>
)}
<NumberInput
label="Actual Distance (KM)"
placeholder="Enter distance"
value={distanceValue}
onChange={(v) => setDistanceValue(String(v ?? ""))}
min={0}
step={0.1}
decimalScale={2}
/>
{(activeRecord?.vehicleAssignments?.length ?? 0) === 0 ? (
<Text size="sm" c="dimmed">Assign a vehicle before entering distance.</Text>
) : (
<Stack gap="sm">
{activeRecord!.vehicleAssignments!.map((a) => {
const v = a.vehicle;
const label = v ? [v.code, v.plateNumber].filter(Boolean).join(" · ") : a.vehicleId;
return (
<NumberInput
key={a.id}
label={`${label}${a.containerNumber ? ` · ${a.containerNumber}` : ""}`}
placeholder="Distance (km)"
value={distanceRows[a.vehicleId] ?? ""}
onChange={(val) =>
setDistanceRows((prev) => ({ ...prev, [a.vehicleId]: String(val ?? "") }))
}
min={0}
step={0.1}
decimalScale={2}
/>
);
})}
<Group justify="space-between">
<Text size="xs" c="dimmed">Total</Text>
<Text size="sm" fw={600}>
{Object.values(distanceRows)
.reduce((s, val) => s + (parseFloat(val) || 0), 0)
.toFixed(2)}{" "}
km
</Text>
</Group>
</Stack>
)}
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={closeDistance}>Cancel</Button>
<Button
onClick={handleSaveDistance}
loading={updateDistanceMutation.isPending}
disabled={!distanceValue}
loading={distanceMutation.isPending}
disabled={Object.values(distanceRows).every((v) => !v)}
>
Save
</Button>

View File

@@ -61,6 +61,7 @@ export interface LastMileRecord {
id: string;
vehicleId: string;
containerNumber?: string | null;
distanceKm?: number | null;
vehicle?: LastMileVehicle | null;
}>;
createdAt: string;
@@ -88,4 +89,11 @@ export const lastMileService = {
id: string,
vehicles: Array<{ vehicleId: string; containerNumber?: string | null }>,
) => api.post<LastMileRecord>(`${LM.BASE}/${id}/vehicles`, { vehicles }),
setDistances: (
id: string,
distances: Array<{ vehicleId: string; distanceKm: number }>,
remainingPayment?: number,
) => api.post<LastMileRecord>(`${LM.BASE}/${id}/distances`, { distances, remainingPayment }),
generateInvoice: (id: string) =>
api.post<unknown>(`${LM.BASE}/${id}/invoice`),
};