Merge branch 'dev' of github.com:Tria-plc/edr-platform into freight_feature/usermanagement

This commit is contained in:
marshal
2026-07-03 11:28:55 +03:00
28 changed files with 585 additions and 166 deletions

View File

@@ -33,6 +33,7 @@ const BookingDetailPage = () => {
onSuccess: () => {
toast.success("Containers allocated");
qc.invalidateQueries({ queryKey: QUERY_KEYS.BOOKINGS.byId(id ?? "") });
qc.invalidateQueries({ queryKey: ["vehicles"] });
},
onError: () => {
toast.error("Failed to allocate containers");

View File

@@ -46,17 +46,22 @@ const FleetResourcePage = () => {
const { viewMode, setViewMode } = useFleetViewMode(slug);
const serverListFilters = useMemo((): FleetListFilters | undefined => {
if (slug !== "wagons" && slug !== "locomotives") return undefined;
const serverFilteredSlugs: FleetResourceSlug[] = ["wagons", "locomotives", "vehicles", "drivers"];
if (!serverFilteredSlugs.includes(slug)) return undefined;
const filters: FleetListFilters = {};
const status = listFilterValues.status;
const currentYardId = listFilterValues.currentYardId;
const availability = listFilterValues.availability;
if (status && status !== "ALL") {
(filters as { status?: string }).status = status;
}
if (currentYardId && currentYardId !== "ALL") {
filters.currentYardId = currentYardId;
}
if (slug === "wagons" && search.trim()) {
if (availability && availability !== "ALL") {
(filters as { availability?: string }).availability = availability;
}
if (slug !== "locomotives" && search.trim()) {
filters.search = search.trim();
}
return filters;
@@ -189,6 +194,7 @@ const FleetResourcePage = () => {
registerFleetOptionLabels("wagonId", dynamicOptions.wagons);
registerFleetOptionLabels("containerId", dynamicOptions.containers);
registerFleetOptionLabels("currentYardId", dynamicOptions.yards);
registerFleetOptionLabels("locationId", dynamicOptions.yards);
}, [dynamicOptions]);
const formFields = useMemo((): FleetFormFieldDef[] => {

View File

@@ -24,6 +24,11 @@ const VEHICLE_STATUS_OPTIONS = [
{ label: "Out of service", value: "OUT_OF_SERVICE" },
];
const VEHICLE_AVAILABILITY_OPTIONS = [
{ label: "Free", value: "FREE" },
{ label: "Busy", value: "BUSY" },
];
export const vehiclesConfig: FleetResourceConfig = {
slug: "vehicles",
label: "Vehicles",
@@ -44,24 +49,25 @@ export const vehiclesConfig: FleetResourceConfig = {
allLabel: "All statuses",
options: VEHICLE_STATUS_OPTIONS,
},
{
key: "availability",
label: "Availability",
allLabel: "All availability",
options: VEHICLE_AVAILABILITY_OPTIONS,
},
],
searchKeys: ["plateNumber", "registrationNumber", "manufacturer", "model", "vehicleType", "status"],
columns: [
{ id: "code", header: "Code", accessorKey: "code", format: "code", size: 110 },
{ id: "plateNumber", header: "Plate Number", accessorKey: "plateNumber", format: "code", size: 130 },
{ id: "powerPlateNo", header: "Power Plate No", accessorKey: "powerPlateNo", format: "code", size: 140 },
{ id: "trailerPlateNo", header: "Trailer Plate No", accessorKey: "trailerPlateNo", format: "code", size: 140 },
{ id: "registrationNumber", header: "Registration", accessorKey: "registrationNumber", format: "code", size: 140 },
{ id: "manufacturer", header: "Manufacturer", accessorKey: "manufacturer", format: "code", size: 140 },
{ id: "model", header: "Model", accessorKey: "model", format: "code", size: 120 },
{ id: "vehicleType", header: "Type", accessorKey: "vehicleType", format: "code", size: 100 },
{ id: "year", header: "Year", accessorKey: "year", format: "number", size: 80 },
{ id: "fuelType", header: "Fuel Type", accessorKey: "fuelType", format: "code", size: 110 },
{ id: "capacity", header: "Capacity (tons)", accessorKey: "capacity", format: "number", size: 130 },
{ id: "assignedDriverName", header: "Assigned Driver", accessorKey: "assignedDriverName", format: "code", size: 140 },
{ id: "estimatedDistanceKm", header: "Est. Distance (KM)", accessorKey: "estimatedDistanceKm", format: "number", size: 150 },
{ id: "actualDistanceKm", header: "Actual Distance (KM)", accessorKey: "actualDistanceKm", format: "number", size: 150 },
{ id: "code", header: "Code", accessorKey: "code", size: 90 },
{ id: "plateNumber", header: "Plate Number", accessorKey: "plateNumber", size: 120 },
{ id: "trailerPlateNo", header: "Trailer Plate No", accessorKey: "trailerPlateNo", size: 130 },
{ id: "manufacturer", header: "Manufacturer", accessorKey: "manufacturer", size: 120 },
{ id: "model", header: "Model", accessorKey: "model", size: 100 },
{ id: "vehicleType", header: "Type", accessorKey: "vehicleType", size: 75 },
{ id: "capacity", header: "Capacity (tons)", accessorKey: "capacity", format: "number", size: 100 },
{ id: "locationId", header: "Location", accessorKey: "locationId", size: 140 },
{ id: "status", header: "Status", accessorKey: "status", format: "statusBadge", size: 100 },
{ id: "availability", header: "Availability", accessorKey: "availability", format: "statusBadge", size: 100 },
],
formFields: [
{ name: "code", label: "Code", type: "text" },
@@ -74,9 +80,11 @@ export const vehiclesConfig: FleetResourceConfig = {
{ name: "year", label: "Year", type: "number", required: true },
{ name: "fuelType", label: "Fuel Type", type: "select", required: true, options: FUEL_TYPE_OPTIONS },
{ name: "capacity", label: "Capacity", type: "number", required: true },
{ name: "locationId", label: "Location", type: "select", dynamicOptions: "yards" },
{ name: "estimatedDistanceKm", label: "Estimated Distance (KM)", type: "number" },
{ name: "actualDistanceKm", label: "Actual Distance (KM)", type: "number" },
{ name: "status", label: "Status", type: "select", required: true, options: VEHICLE_STATUS_OPTIONS },
{ name: "availability", label: "Availability", type: "select", required: true, options: VEHICLE_AVAILABILITY_OPTIONS },
{ name: "description", label: "Description", type: "textarea" },
],
emptyValues: {
@@ -90,11 +98,13 @@ export const vehiclesConfig: FleetResourceConfig = {
year: new Date().getFullYear(),
fuelType: "DIESEL",
capacity: 0,
locationId: null,
estimatedDistanceKm: "",
actualDistanceKm: "",
status: "ACTIVE",
availability: "FREE",
description: "",
},
};
export { VEHICLE_TYPE_OPTIONS, FUEL_TYPE_OPTIONS, VEHICLE_STATUS_OPTIONS };
export { VEHICLE_TYPE_OPTIONS, FUEL_TYPE_OPTIONS, VEHICLE_STATUS_OPTIONS, VEHICLE_AVAILABILITY_OPTIONS };

View File

@@ -8,6 +8,7 @@ import {
Printer,
RefreshCw,
Ruler,
Trash,
Truck,
} from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -124,42 +125,45 @@ const InfoRow = ({ label, value }: { label: string; value: string }) => (
</Stack>
);
const BookingInfo = ({ record }: { record: FirstMileRecord }) => (
<Card radius="md" padding="md" withBorder bg="var(--mantine-color-gray-0)">
<Stack gap="sm">
<Group justify="space-between">
<Text fw={600}>{bookingRef(record)}</Text>
<Group gap="xs">
<Badge color={STATUS_META[record.status].color} variant="light" size="sm">
{STATUS_META[record.status].label}
</Badge>
<Badge
color={isAssigned(record) ? "green" : "orange"}
variant="light"
size="sm"
>
{isAssigned(record) ? "Assigned" : "Unassigned"}
</Badge>
const BookingInfo = ({ record }: { record: FirstMileRecord }) => {
const hasPickupAddress = record.booking?.firstMilePickupAddress != null;
return (
<Card radius="md" padding="md" withBorder bg="var(--mantine-color-gray-0)">
<Stack gap="sm">
<Group justify="space-between">
<Text fw={600}>{bookingRef(record)}</Text>
<Group gap="xs">
<Badge color={STATUS_META[record.status].color} variant="light" size="sm">
{STATUS_META[record.status].label}
</Badge>
<Badge
color={isAssigned(record) ? "green" : "orange"}
variant="light"
size="sm"
>
{isAssigned(record) ? "Assigned" : "Unassigned"}
</Badge>
</Group>
</Group>
</Group>
<SimpleGrid cols={2} spacing="sm">
<InfoRow label="Customer" value={customerName(record)} />
<InfoRow label="Service type" value={serviceTypeName(record)} />
<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="Contact" value={contactPersonName(record)} />
<InfoRow label="Phone" value={contactPhone(record)} />
<InfoRow label="Requested date" value={requestedDate(record)} />
<InfoRow label="Assigned vehicle" value={vehicleLabel(record) ?? "—"} />
<InfoRow label="Est. Distance (KM)" value={record.estimatedKm != null ? String(record.estimatedKm) : "—"} />
<InfoRow label="Actual Distance (KM)" value={record.exactKm != null ? String(record.exactKm) : "—"} />
</SimpleGrid>
</Stack>
</Card>
);
<SimpleGrid cols={2} spacing="sm">
<InfoRow label="Customer" value={customerName(record)} />
<InfoRow label="Service type" value={serviceTypeName(record)} />
{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="Contact" value={contactPersonName(record)} />
<InfoRow label="Phone" value={contactPhone(record)} />
<InfoRow label="Requested date" value={requestedDate(record)} />
<InfoRow label="Assigned vehicle" value={vehicleLabel(record) ?? "—"} />
<InfoRow label="Est. Distance (KM)" value={record.estimatedKm != null ? String(record.estimatedKm) : "—"} />
<InfoRow label="Actual Distance (KM)" value={record.exactKm != null ? String(record.exactKm) : "—"} />
</SimpleGrid>
</Stack>
</Card>
);
};
const tripSlipRows = (record: FirstMileRecord): [string, string][] => [
["Customer", customerName(record)],
@@ -355,9 +359,9 @@ const FirstMilePage = () => {
});
const { data: vehiclesData } = useQuery({
queryKey: ["vehicles", "list"],
queryKey: ["vehicles", "free"],
queryFn: async () => {
const res = await vehiclesService.getAll({ status: "ACTIVE" });
const res = await vehiclesService.getAll({ status: "ACTIVE", availability: "FREE" });
return res.data;
},
});
@@ -400,6 +404,7 @@ const FirstMilePage = () => {
firstMileService.update(id, data),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.list() });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
},
onError: () => {
toast({ title: "Update failed", variant: "destructive" });
@@ -421,6 +426,17 @@ const FirstMilePage = () => {
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => firstMileService.remove(id),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.list() });
toast({ title: "Record deleted", description: "First-mile record removed successfully." });
},
onError: () => {
toast({ title: "Delete failed", variant: "destructive" });
},
});
const acceptMutation = useMutation({
mutationFn: async ({ reference, vehicleId }: { reference: string; vehicleId: string | null }) => {
const res = await firstMileService.accept(reference);
@@ -433,6 +449,7 @@ const FirstMilePage = () => {
},
onSuccess: () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.ROOT });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
toast({ title: "Booking accepted", description: "First-mile leg created successfully." });
closeAccept();
},
@@ -449,6 +466,7 @@ const FirstMilePage = () => {
onSuccess: () => {
toast({ title: "Containers allocated" });
void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.byId(containerAllocationFirstMileId ?? "") });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
setContainerAllocationOpen(false);
setContainerAllocationFirstMileId(null);
},
@@ -470,9 +488,14 @@ const FirstMilePage = () => {
const firstMileEligiblePaidBookings = useMemo(
() =>
paidBookings.filter(
(booking) =>
booking.tradeDirection === "EXPORT" &&
!existingFirstMileBookingIds.has(booking.id),
(booking) => {
if (existingFirstMileBookingIds.has(booking.id)) return false;
if (booking.paymentStatus !== "PAID") return false;
if (booking.tradeDirection !== "EXPORT") return false;
const hasPickupAddress = booking.firstMilePickupAddress?.trim() ?? false;
const includesFirstMile = booking.serviceType?.includesFirstMile ?? false;
return hasPickupAddress || includesFirstMile;
},
),
[existingFirstMileBookingIds, paidBookings],
);
@@ -843,6 +866,7 @@ const FirstMilePage = () => {
const assigned = isAssigned(row.original);
const nextStatus = NEXT_STATUS[row.original.status];
const canPrint = row.original.status !== "PAYMENT_PENDING";
const isPaid = (row.original as any).paid;
const canReceiveToWarehouse = row.original.status === "RECEIVED_TO_PORT";
return (
<Group gap={4} justify="flex-end" wrap="nowrap">
@@ -903,6 +927,22 @@ const FirstMilePage = () => {
Print trip slip
</Menu.Item>
)}
{!isPaid && (
<>
<Menu.Divider />
<Menu.Item
leftSection={<Trash size={15} />}
color="red"
onClick={() => {
if (confirm(`Delete first-mile record ${bookingRef(row.original)}?`)) {
deleteMutation.mutate(row.original.id);
}
}}
>
Delete
</Menu.Item>
</>
)}
</Menu.Dropdown>
</Menu>
</Group>
@@ -1022,11 +1062,17 @@ const FirstMilePage = () => {
<Divider />
<Select
label="Vehicle"
placeholder="Select a vehicle"
placeholder={vehicleOptions.length === 0 ? "No free vehicles" : "Select a vehicle"}
description={
vehicleOptions.length === 0
? "No free vehicles available — all vehicles are busy, in maintenance, or retired. Free up a vehicle in Fleet Vehicles first."
: undefined
}
data={vehicleOptions}
value={vehicleValue}
onChange={setVehicleValue}
searchable
disabled={vehicleOptions.length === 0}
/>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={closeAssign}>Cancel</Button>
@@ -1200,12 +1246,18 @@ const FirstMilePage = () => {
<Divider />
<Select
label="Assign Vehicle (optional)"
placeholder="Select a vehicle"
placeholder={vehicleOptions.length === 0 ? "No free vehicles" : "Select a vehicle"}
description={
vehicleOptions.length === 0
? "No free vehicles available — you can still accept and assign a vehicle later."
: undefined
}
data={vehicleOptions}
value={acceptVehicleValue}
onChange={setAcceptVehicleValue}
searchable
clearable
disabled={vehicleOptions.length === 0}
/>
<Group justify="space-between" gap="sm">
<Button variant="subtle" onClick={() => setAcceptStep(1)}> Back</Button>

View File

@@ -6,6 +6,7 @@ import {
Printer,
RefreshCw,
Ruler,
Trash,
Truck,
} from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -172,38 +173,41 @@ const InfoRow = ({ label, value }: { label: string; value: string }) => (
</Stack>
);
const BookingInfo = ({ record }: { record: LastMileRecord }) => (
<Card radius="md" padding="md" withBorder bg="var(--mantine-color-gray-0)">
<Stack gap="sm">
<Group justify="space-between">
<Text fw={600}>{bookingRef(record)}</Text>
<Group gap="xs">
<Badge color={STATUS_META[record.status].color} variant="light" size="sm">
{STATUS_META[record.status].label}
</Badge>
<Badge color={isAssigned(record) ? "green" : "orange"} variant="light" size="sm">
{isAssigned(record) ? "Assigned" : "Unassigned"}
</Badge>
const BookingInfo = ({ record }: { record: LastMileRecord }) => {
const hasDeliveryAddress = record.booking?.lastMileDeliveryAddress != null;
return (
<Card radius="md" padding="md" withBorder bg="var(--mantine-color-gray-0)">
<Stack gap="sm">
<Group justify="space-between">
<Text fw={600}>{bookingRef(record)}</Text>
<Group gap="xs">
<Badge color={STATUS_META[record.status].color} variant="light" size="sm">
{STATUS_META[record.status].label}
</Badge>
<Badge color={isAssigned(record) ? "green" : "orange"} variant="light" size="sm">
{isAssigned(record) ? "Assigned" : "Unassigned"}
</Badge>
</Group>
</Group>
</Group>
<SimpleGrid cols={2} spacing="sm">
<InfoRow label="Customer" value={customerName(record)} />
<InfoRow label="Service type" value={serviceTypeName(record)} />
<InfoRow label="Pickup (origin yard)" value={originYardName(record)} />
<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="Contact" value={contactPersonName(record)} />
<InfoRow label="Phone" value={contactPhone(record)} />
<InfoRow label="Requested date" value={requestedDate(record)} />
<InfoRow label="Assigned vehicle" value={vehicleLabel(record) ?? "—"} />
<InfoRow label="Est. Distance (KM)" value={record.estimatedKm != null ? String(record.estimatedKm) : "—"} />
<InfoRow label="Actual Distance (KM)" value={record.exactKm != null ? String(record.exactKm) : "—"} />
</SimpleGrid>
</Stack>
</Card>
);
<SimpleGrid cols={2} spacing="sm">
<InfoRow label="Customer" value={customerName(record)} />
<InfoRow label="Service type" value={serviceTypeName(record)} />
<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="Contact" value={contactPersonName(record)} />
<InfoRow label="Phone" value={contactPhone(record)} />
<InfoRow label="Requested date" value={requestedDate(record)} />
<InfoRow label="Assigned vehicle" value={vehicleLabel(record) ?? "—"} />
<InfoRow label="Est. Distance (KM)" value={record.estimatedKm != null ? String(record.estimatedKm) : "—"} />
<InfoRow label="Actual Distance (KM)" value={record.exactKm != null ? String(record.exactKm) : "—"} />
</SimpleGrid>
</Stack>
</Card>
);
};
const tripSlipRows = (record: LastMileRecord): [string, string][] => [
["Customer", customerName(record)],
@@ -392,9 +396,9 @@ const LastMilePage = () => {
});
const { data: vehiclesData } = useQuery({
queryKey: ["vehicles", "list"],
queryKey: ["vehicles", "free"],
queryFn: async () => {
const res = await vehiclesService.getAll({ status: "ACTIVE" });
const res = await vehiclesService.getAll({ status: "ACTIVE", availability: "FREE" });
return res.data;
},
});
@@ -408,6 +412,10 @@ const LastMilePage = () => {
});
const records = listData?.data ?? [];
const existingLastMileBookingIds = useMemo(
() => new Set(records.map((record) => record.bookingId)),
[records],
);
const needsDriverLookup = records.some((record) => record.vehicle?.assignedDriverId);
@@ -447,6 +455,7 @@ const LastMilePage = () => {
lastMileService.update(id, data),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
},
onError: () => {
toast({ title: "Update failed", variant: "destructive" });
@@ -468,12 +477,24 @@ const LastMilePage = () => {
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => lastMileService.remove(id),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.list() });
toast({ title: "Record deleted", description: "Last-mile record removed successfully." });
},
onError: () => {
toast({ title: "Delete failed", variant: "destructive" });
},
});
const allocateMutation = useMutation({
mutationFn: (data: Array<{ containerId: string; vehicleId: string }>) =>
api.post(`/last-mile/${activeId}/allocate-containers`, data),
onSuccess: () => {
toast({ title: "Containers allocated", variant: "default" });
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.byId(activeId ?? "") });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
closeAllocation();
},
onError: () => {
@@ -489,15 +510,16 @@ const LastMilePage = () => {
const arrivalQueue = arrivalQueueData ?? [];
const filteredArrivalQueue = useMemo(() => {
let filtered = arrivalQueue.filter((item) => !existingLastMileBookingIds.has(item.bookingId));
const term = arrivalSearch.trim().toLowerCase();
if (!term) return arrivalQueue;
return arrivalQueue.filter((item) =>
if (!term) return filtered;
return filtered.filter((item) =>
[item.bookingReference, item.customer, item.cargo, item.warehouse, item.yard]
.join(" ")
.toLowerCase()
.includes(term),
);
}, [arrivalQueue, arrivalSearch]);
}, [arrivalQueue, arrivalSearch, existingLastMileBookingIds]);
const acceptMutation = useMutation({
mutationFn: async ({ items, vehicleId }: { items: ArrivalQueueItem[]; vehicleId: string | null }) => {
@@ -511,6 +533,7 @@ const LastMilePage = () => {
},
onSuccess: (created) => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
toast({
title: "Last-mile leg created",
description: `${created.length} ${created.length === 1 ? "delivery" : "deliveries"} accepted successfully.`,
@@ -928,6 +951,8 @@ const LastMilePage = () => {
const assigned = isAssigned(row.original);
const nextStatus = NEXT_STATUS[row.original.status];
const canPrint = row.original.status !== "PAYMENT_PENDING";
const isPaid = (row.original as any).paid;
const delivered = row.original.status === "DELIVERED";
const releaseRow =
pickupReadyByBooking.get(row.original.bookingId) ?? pickupReadyByBooking.get(bookingRef(row.original));
const truckArrivalLabel = releaseRow?.releaseOrderReference ? "Truck Leaving" : "Truck Arrival";
@@ -950,14 +975,14 @@ const LastMilePage = () => {
<Menu.Divider />
<Menu.Item
leftSection={<Truck size={15} />}
disabled={assigned}
disabled={assigned || delivered}
onClick={() => openAssign(row.original.id)}
>
Assign
</Menu.Item>
<Menu.Item
leftSection={<RefreshCw size={15} />}
disabled={!assigned}
disabled={!assigned || delivered}
onClick={() => openAssign(row.original.id)}
>
Reassign
@@ -978,6 +1003,7 @@ const LastMilePage = () => {
</Menu.Item>
<Menu.Item
leftSection={<Ruler size={15} />}
disabled={delivered}
onClick={() => openDistance(row.original.id)}
>
Add distance
@@ -990,6 +1016,23 @@ const LastMilePage = () => {
Print trip slip
</Menu.Item>
)}
{!isPaid && (
<>
<Menu.Divider />
<Menu.Item
leftSection={<Trash size={15} />}
color="red"
disabled={delivered}
onClick={() => {
if (confirm(`Delete last-mile record ${bookingRef(row.original)}?`)) {
deleteMutation.mutate(row.original.id);
}
}}
>
Delete
</Menu.Item>
</>
)}
</Menu.Dropdown>
</Menu>
</Group>
@@ -1187,12 +1230,18 @@ const LastMilePage = () => {
<Divider />
<Select
label="Assign Vehicle (optional)"
placeholder="Select a vehicle"
placeholder={vehicleOptions.length === 0 ? "No free vehicles" : "Select a vehicle"}
description={
vehicleOptions.length === 0
? "No free vehicles available — you can still accept and assign a vehicle later."
: undefined
}
data={vehicleOptions}
value={acceptVehicleValue}
onChange={setAcceptVehicleValue}
searchable
clearable
disabled={vehicleOptions.length === 0}
/>
<Group justify="space-between" gap="sm">
<Button variant="subtle" onClick={() => setAcceptStep(1)}> Back</Button>
@@ -1235,11 +1284,17 @@ const LastMilePage = () => {
<Divider />
<Select
label="Vehicle"
placeholder="Select a vehicle"
placeholder={vehicleOptions.length === 0 ? "No free vehicles" : "Select a vehicle"}
description={
vehicleOptions.length === 0
? "No free vehicles available — all vehicles are busy, in maintenance, or retired. Free up a vehicle in Fleet Vehicles first."
: undefined
}
data={vehicleOptions}
value={vehicleValue}
onChange={setVehicleValue}
searchable
disabled={vehicleOptions.length === 0}
/>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={closeAssign}>Cancel</Button>