import { type ReactNode, useMemo, useState } from "react"; import { ArrowRight, ChevronRight, Eye, MoreHorizontal, Printer, RefreshCw, Truck, } from "lucide-react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { ColumnDef } from "@edr/ui-common"; import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common"; import { ActionIcon, Badge, Box, Button, Card, Checkbox, Divider, Group, Menu, Modal, Select, SimpleGrid, Stack, Text, TextInput, UnstyledButton, } from "@mantine/core"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; import { useToast } from "@/hooks/use-toast"; import { FIRST_MILE_STATUSES, type FirstMileApiStatus, type FirstMileRecord, firstMileService, } from "@/services/first-mile.service"; import { bookingsService } from "@/services/bookings.service"; import { vehiclesService } from "@/services/vehicles.service"; import type { BookingDetail } from "@/types/booking"; const formatPrice = (amount: number) => `ETB ${amount.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, })}`; const STATUS_META: Record = { PAYMENT_PENDING: { label: "Payment Pending", color: "yellow" }, READY_TO_TRANSIT: { label: "Ready to Transit", color: "blue" }, IN_TRANSIT: { label: "In Transit", color: "indigo" }, RECEIVED_TO_PORT: { label: "Received to Port", color: "green" }, }; const NEXT_STATUS: Partial> = { PAYMENT_PENDING: "READY_TO_TRANSIT", READY_TO_TRANSIT: "IN_TRANSIT", IN_TRANSIT: "RECEIVED_TO_PORT", }; type AssignmentStatus = "ASSIGNED" | "UNASSIGNED"; type StatusFilter = "ALL" | FirstMileApiStatus | AssignmentStatus; const FILTER_OPTIONS: { value: StatusFilter; label: string }[] = [ { value: "ALL", label: "All" }, ...FIRST_MILE_STATUSES.map((s) => ({ value: s as StatusFilter, label: STATUS_META[s].label })), { value: "ASSIGNED", label: "Assigned" }, { value: "UNASSIGNED", label: "Unassigned" }, ]; const vehicleLabel = (record: FirstMileRecord) => { if (!record.vehicle) return null; const v = record.vehicle; const parts = [`${v.manufacturer} ${v.model}`, v.plateNumber]; if (v.code) parts.unshift(v.code); const plates = [v.powerPlateNo, v.trailerPlateNo].filter(Boolean).join(" / "); if (plates) parts.push(plates); return parts.join(" · "); }; const isAssigned = (record: FirstMileRecord) => Boolean(record.vehicleId); // Map API record → display fields used in modals and trip slip const bookingRef = (r: FirstMileRecord) => r.booking?.reference ?? r.bookingId; const customerName = (r: FirstMileRecord) => r.booking?.company?.name ?? "—"; const pickupLocation = (r: FirstMileRecord) => r.booking?.firstMilePickupAddress ?? "—"; const cargoDesc = (r: FirstMileRecord) => { const parts = [r.booking?.cargoType?.name ?? r.booking?.cargoFreeText].filter(Boolean); if (r.booking?.cargoTotalWeightVgm) parts.push(`${r.booking.cargoTotalWeightVgm} t`); return parts.join(" · ") || "—"; }; const priceAmount = (r: FirstMileRecord) => r.booking?.totalAmount ?? r.advancedPayment; // First-mile destination is the origin yard (pickup → origin yard) const destinationYardName = (r: FirstMileRecord) => r.booking?.originYard?.label ?? "—"; const contactPersonName = (r: FirstMileRecord) => r.booking?.company?.contactPersonName ?? "—"; const contactPhone = (r: FirstMileRecord) => r.booking?.company?.contactPersonPhone ?? r.booking?.company?.phone ?? "—"; const requestedDate = (r: FirstMileRecord) => { const d = r.booking?.scheduledDate; return d ? new Date(d).toISOString().slice(0, 10) : "—"; }; const serviceTypeName = (r: FirstMileRecord) => r.booking?.serviceType?.label ?? "—"; const InfoRow = ({ label, value }: { label: string; value: string }) => ( {label} {value} ); const BookingInfo = ({ record }: { record: FirstMileRecord }) => ( {bookingRef(record)} {STATUS_META[record.status].label} {isAssigned(record) ? "Assigned" : "Unassigned"} ); const tripSlipRows = (record: FirstMileRecord): [string, string][] => [ ["Customer", customerName(record)], ["Service", serviceTypeName(record)], ["Pickup location", pickupLocation(record)], ["Destination yard", destinationYardName(record)], ["Cargo", cargoDesc(record)], ["Price", formatPrice(priceAmount(record))], ["Vehicle", vehicleLabel(record) ?? "Unassigned"], ["Contact", `${contactPersonName(record)} · ${contactPhone(record)}`], ["Requested date", requestedDate(record)], ["Status", STATUS_META[record.status].label], ]; const SampleStamp = () => ( EDR FREIGHT APPROVED OPERATIONS ); const SignatureBlock = ({ title, stamp }: { title: string; stamp?: ReactNode }) => ( {title} Name: Signature: {stamp && ( {stamp} )} ); const TripSlipDocument = ({ record }: { record: FirstMileRecord }) => ( EDR Freight First Mile Trip Slip {bookingRef(record)} {requestedDate(record)} {tripSlipRows(record).map(([label, value]) => ( ))} } /> ); const escapeHtml = (v: string) => v.replace(/&/g, "&").replace(//g, ">"); const buildTripSlipHtml = (record: FirstMileRecord) => { const rows = tripSlipRows(record) .map(([l, v]) => `${escapeHtml(l)}${escapeHtml(v)}`) .join(""); const sig = (title: string, withStamp: boolean) => `
${title}
Name:
Signature:
${withStamp ? '
EDR FREIGHTAPPROVEDOPERATIONS
' : ""}
`; return ` Trip Slip ${escapeHtml(bookingRef(record))}

EDR Freight

First Mile Trip Slip

${escapeHtml(bookingRef(record))}${escapeHtml(requestedDate(record))}
${rows}
Acknowledgement
${sig("Driver", false)}${sig("Operator", true)}
`; }; const FirstMilePage = () => { const { toast } = useToast(); const qc = useQueryClient(); const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState("ALL"); const [rowSelection, setRowSelection] = useState>({}); const [assignOpen, setAssignOpen] = useState(false); const [bulkMode, setBulkMode] = useState(false); const [detailOpen, setDetailOpen] = useState(false); const [tripSlipOpen, setTripSlipOpen] = useState(false); const [tripSlipRecord, setTripSlipRecord] = useState(null); const [activeId, setActiveId] = useState(null); const [vehicleValue, setVehicleValue] = useState(null); const [acceptOpen, setAcceptOpen] = useState(false); const [acceptStep, setAcceptStep] = useState<1 | 2>(1); const [selectedBooking, setSelectedBooking] = useState(null); const [acceptVehicleValue, setAcceptVehicleValue] = useState(null); const [bookingSearch, setBookingSearch] = useState(""); const { data: listData, isLoading } = useQuery({ queryKey: QUERY_KEYS.FIRST_MILE.list(), queryFn: async () => { const res = await firstMileService.list(); return res.data; }, }); const { data: vehiclesData } = useQuery({ queryKey: ["vehicles", "list"], queryFn: async () => { const res = await vehiclesService.getAll({ status: "ACTIVE" }); return res.data; }, }); const { data: paidBookingsData, isLoading: bookingsLoading } = useQuery({ queryKey: QUERY_KEYS.BOOKINGS.list({ status: "PAID" }), queryFn: () => bookingsService.list({ status: "PAID", pageSize: 100 }), enabled: acceptOpen, }); const paidBookings = paidBookingsData?.items ?? []; const records = listData?.data ?? []; const vehicleOptions = useMemo( () => (Array.isArray(vehiclesData) ? vehiclesData : []).map((v) => { const parts = [`${v.manufacturer} ${v.model}`, v.plateNumber]; if (v.code) parts.unshift(v.code); const plates = [v.powerPlateNo, v.trailerPlateNo].filter(Boolean).join(" / "); if (plates) parts.push(plates); return { value: v.id, label: parts.join(" · ") }; }), [vehiclesData], ); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: { status?: FirstMileApiStatus; vehicleId?: string | null } }) => firstMileService.update(id, data), onSuccess: () => { void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.ROOT }); }, onError: () => { toast({ title: "Update failed", variant: "destructive" }); }, }); const acceptMutation = useMutation({ mutationFn: async ({ reference, vehicleId }: { reference: string; vehicleId: string | null }) => { const res = await firstMileService.accept(reference); const created = res.data; if (vehicleId) await firstMileService.update(created.id, { vehicleId }); return created; }, onSuccess: () => { void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.ROOT }); toast({ title: "Booking accepted", description: "First-mile leg created successfully." }); closeAccept(); }, onError: () => { toast({ title: "Accept failed", variant: "destructive" }); }, }); const activeRecord = useMemo( () => records.find((r) => r.id === activeId) ?? null, [records, activeId], ); const selectedIds = useMemo( () => Object.keys(rowSelection).filter((id) => rowSelection[id]), [rowSelection], ); const filteredPaidBookings = useMemo(() => { const term = bookingSearch.trim().toLowerCase(); if (!term) return paidBookings; return paidBookings.filter((b) => [b.reference, b.company?.name, b.company?.companyName] .join(" ") .toLowerCase() .includes(term), ); }, [paidBookings, bookingSearch]); const openAccept = () => { setAcceptOpen(true); setAcceptStep(1); setSelectedBooking(null); setAcceptVehicleValue(null); setBookingSearch(""); }; const closeAccept = () => { setAcceptOpen(false); setAcceptStep(1); setSelectedBooking(null); setAcceptVehicleValue(null); setBookingSearch(""); }; const handleAcceptConfirm = () => { if (!selectedBooking) return; acceptMutation.mutate({ reference: selectedBooking.reference, vehicleId: acceptVehicleValue }); }; const matchesFilter = (r: FirstMileRecord) => { switch (statusFilter) { case "ALL": return true; case "ASSIGNED": return isAssigned(r); case "UNASSIGNED": return !isAssigned(r); default: return r.status === statusFilter; } }; const statusCounts = useMemo(() => { const counts: Record = { ALL: records.length, PAYMENT_PENDING: 0, READY_TO_TRANSIT: 0, IN_TRANSIT: 0, RECEIVED_TO_PORT: 0, ASSIGNED: 0, UNASSIGNED: 0, }; for (const r of records) { counts[r.status] = (counts[r.status] ?? 0) + 1; if (isAssigned(r)) counts.ASSIGNED += 1; else counts.UNASSIGNED += 1; } return counts; }, [records]); const filteredRecords = useMemo(() => { const term = search.trim().toLowerCase(); return records.filter((r) => { if (!matchesFilter(r)) return false; if (!term) return true; return [bookingRef(r), customerName(r), pickupLocation(r), cargoDesc(r)] .join(" ") .toLowerCase() .includes(term); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [records, search, statusFilter]); const pageCount = Math.max(1, Math.ceil(filteredRecords.length / pagination.pageSize)); const pagedRecords = useMemo(() => { const start = pagination.pageIndex * pagination.pageSize; return filteredRecords.slice(start, start + pagination.pageSize); }, [filteredRecords, pagination]); const openAssign = (id: string | null) => { const resolved = id ?? filteredRecords.find((r) => !isAssigned(r))?.id ?? null; setBulkMode(false); setActiveId(resolved); setVehicleValue(null); setAssignOpen(true); }; const openBulkAssign = () => { setBulkMode(true); setActiveId(null); setVehicleValue(null); setAssignOpen(true); }; const closeAssign = () => { setAssignOpen(false); setBulkMode(false); setActiveId(null); setVehicleValue(null); }; const handleAssign = () => { if (!vehicleValue) { toast({ title: "Select a vehicle", description: "Choose a vehicle to assign.", variant: "destructive" }); return; } const targetIds = bulkMode ? selectedIds : [activeId ?? filteredRecords.find((r) => !isAssigned(r))?.id].filter((id): id is string => Boolean(id)); if (!targetIds.length) return; const selectedLabel = vehicleOptions.find((o) => o.value === vehicleValue)?.label ?? vehicleValue; Promise.all(targetIds.map((id) => updateMutation.mutateAsync({ id, data: { vehicleId: vehicleValue } }))) .then(() => { toast({ title: "Vehicle assigned", description: bulkMode ? `${targetIds.length} pickups → ${selectedLabel}` : selectedLabel, }); if (bulkMode) setRowSelection({}); closeAssign(); }) .catch(() => void 0); }; const handleAdvanceStatus = (record: FirstMileRecord) => { const next = NEXT_STATUS[record.status]; if (!next) return; updateMutation.mutate( { id: record.id, data: { status: next } }, { onSuccess: () => toast({ title: "Status updated", description: `${bookingRef(record)} → ${STATUS_META[next].label}` }), }, ); }; const handlePrintTripSlip = (record: FirstMileRecord) => { setTripSlipRecord(record); setTripSlipOpen(true); }; const printTripSlip = () => { if (!tripSlipRecord) return; const win = window.open("", "_blank", "width=820,height=920"); if (!win) { toast({ title: "Pop-up blocked", description: "Allow pop-ups to print the trip slip.", variant: "destructive" }); return; } win.document.write(buildTripSlipHtml(tripSlipRecord)); win.document.close(); }; const columns = useMemo((): ColumnDef[] => { const headerClassName = ruleEngineTable.headerCell; const cellClassName = ruleEngineTable.bodyCell; return [ { id: "select", size: 40, meta: { headerClassName, cellClassName }, header: ({ table }) => ( table.toggleAllPageRowsSelected(e.currentTarget.checked)} /> ), cell: ({ row }) => ( ), }, { id: "bookingRef", header: "Booking", meta: { headerClassName, cellClassName }, cell: ({ row }) => {bookingRef(row.original)}, }, { id: "customer", header: "Customer", meta: { headerClassName, cellClassName }, cell: ({ row }) => customerName(row.original), }, { id: "pickup", header: "Pickup", meta: { headerClassName, cellClassName }, cell: ({ row }) => pickupLocation(row.original), }, { id: "destination", header: "Destination", meta: { headerClassName, cellClassName }, cell: ({ row }) => destinationYardName(row.original), }, { id: "cargo", header: "Cargo", meta: { headerClassName, cellClassName }, cell: ({ row }) => cargoDesc(row.original), }, { id: "price", header: "Price", meta: { headerClassName, cellClassName }, cell: ({ row }) => formatPrice(priceAmount(row.original)), }, { id: "vehicle", header: "Vehicle", meta: { headerClassName, cellClassName }, cell: ({ row }) => vehicleLabel(row.original) ?? , }, { id: "status", header: "Status", meta: { headerClassName, cellClassName }, cell: ({ row }) => { const meta = STATUS_META[row.original.status]; return {meta.label}; }, }, { id: "assignment", header: "Assignment", meta: { headerClassName, cellClassName }, cell: ({ row }) => ( {isAssigned(row.original) ? "Assigned" : "Unassigned"} ), }, { id: "actions", header: "Actions", meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` }, cell: ({ row }) => { const assigned = isAssigned(row.original); const nextStatus = NEXT_STATUS[row.original.status]; const canPrint = row.original.status !== "PAYMENT_PENDING"; return ( } disabled={!nextStatus} onClick={() => handleAdvanceStatus(row.original)} > {nextStatus ? `Mark ${STATUS_META[nextStatus].label}` : STATUS_META[row.original.status].label} } disabled={assigned} onClick={() => openAssign(row.original.id)} > Assign } disabled={!assigned} onClick={() => openAssign(row.original.id)} > Reassign } onClick={() => { setActiveId(row.original.id); setDetailOpen(true); }} > View detail {canPrint && ( } onClick={() => handlePrintTripSlip(row.original)} > Print trip slip )} ); }, }, ]; // eslint-disable-next-line react-hooks/exhaustive-deps }, [vehicleOptions]); return ( setSearch(e.currentTarget.value)} w={260} /> {selectedIds.length > 0 && ( )} {FILTER_OPTIONS.map((option) => { const active = statusFilter === option.value; return ( ); })} row.id, state: { pagination, rowSelection }, onPaginationChange: setPagination, onRowSelectionChange: setRowSelection, }} containerClassName="border-0 shadow-none bg-transparent" footer={({ table, pagination: fp }) => ( )} /> {/* Assign / Reassign modal */} Assign Vehicle} size="lg" radius="lg" centered > {bulkMode ? ( Assigning a vehicle to{" "} {selectedIds.length}{" "} selected {selectedIds.length === 1 ? "pickup" : "pickups"}. ) : activeRecord ? ( ) : ( No unassigned pickups available. )} )} ); }; export default FirstMilePage;