import { useState, useMemo } from "react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { Box, Button, Group, Loader, Select, Stack, Table, Text, Alert, } from "@mantine/core"; import { AlertCircle } from "lucide-react"; import toast from "react-hot-toast"; import { vehiclesService } from "@/services/vehicles.service"; export interface LastMileContainerRow { id: string; type: string; qty: number; } /** One vehicle (with trailer) carries at most this many containers. */ const CONTAINERS_PER_VEHICLE = 2; export interface LastMileContainerAllocationTableProps { containers: LastMileContainerRow[]; onSave: (allocations: Array<{ containerId: string; vehicleId: string }>) => Promise; } /** * Manual container-to-vehicle allocation table for last-mile deliveries. * Displays containers with type/qty, vehicle dropdown per row, and save action. */ export function LastMileContainerAllocationTable({ containers, onSave, }: LastMileContainerAllocationTableProps) { const [allocations, setAllocations] = useState>( () => containers.reduce((acc, c) => ({ ...acc, [c.id]: null }), {}), ); const { data: vehicles = [], isLoading: vehiclesLoading } = useQuery({ queryKey: ["vehicles", "free"], queryFn: () => vehiclesService.getAll({ status: "ACTIVE", availability: "FREE" }).then((r) => r.data), }); const vehicleOptions = useMemo( () => vehicles.map((v) => ({ value: v.id, label: `${v.plateNumber} (${v.vehicleType})`, description: `${v.model} · ${v.manufacturer}`, })), [vehicles], ); const saveAllocation = useMutation({ mutationFn: async () => { const mappings = containers .filter((c) => allocations[c.id]) .map((c) => ({ containerId: c.id, vehicleId: allocations[c.id]!, })); if (mappings.length === 0) { throw new Error("No containers allocated to vehicles"); } await onSave(mappings); }, onSuccess: () => { toast.success("Container allocations saved"); setAllocations( containers.reduce((acc, c) => ({ ...acc, [c.id]: null }), {}), ); }, onError: (error) => { toast.error( error instanceof Error ? error.message : "Failed to save allocations", ); }, }); const allocatedCount = Object.values(allocations).filter(Boolean).length; // Containers already loaded onto each vehicle, to enforce the 2-per-vehicle cap. const loadByVehicle = useMemo(() => { const map: Record = {}; for (const c of containers) { const v = allocations[c.id]; if (v) map[v] = (map[v] ?? 0) + (c.qty || 1); } return map; }, [allocations, containers]); /** Options for a given row: a vehicle is disabled if assigning this container * to it would exceed its 2-container capacity. */ const optionsForRow = (row: LastMileContainerRow) => vehicleOptions.map((o) => { const already = loadByVehicle[o.value] ?? 0; const selfHere = allocations[row.id] === o.value ? row.qty || 1 : 0; const over = already - selfHere + (row.qty || 1) > CONTAINERS_PER_VEHICLE; return { ...o, disabled: over }; }); if (vehiclesLoading) { return ( ); } return ( {vehicles.length === 0 && ( } color="yellow"> No free vehicles available. Free up or add vehicles before allocating containers. )} Container ID Type Qty Assigned Vehicle {containers.map((container) => ( {container.id} {container.type} {container.qty}
{allocatedCount} of {containers.length} containers allocated · max{" "} {CONTAINERS_PER_VEHICLE} per vehicle
); }