import { useEffect, useState } from "react"; import { Badge, Box, Button, Card, Group, Modal, NumberInput, Stack, Text, Textarea, } from "@mantine/core"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { ColumnDef } from "@edr/ui-common"; import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common"; import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; import { useToast } from "@/hooks/use-toast"; import { useAuth } from "@/auth/useAuth"; import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions"; import { lastMileRequestsService, type LastMileRequest, type LastMileRequestStatus, } from "@/services/last-mile-requests.service"; const STATUS_META: Record = { AWAITING_CONFIRMATION: { label: "Awaiting Confirmation", color: "gray" }, SUBMITTED: { label: "Submitted", color: "yellow" }, APPROVED: { label: "Approved", color: "green" }, REJECTED: { label: "Rejected", color: "red" }, }; type StatusFilter = "ALL" | LastMileRequestStatus; const FILTER_OPTIONS: { value: StatusFilter; label: string }[] = [ { value: "SUBMITTED", label: "Submitted" }, { value: "APPROVED", label: "Approved" }, { value: "REJECTED", label: "Rejected" }, { value: "AWAITING_CONFIRMATION", label: "Awaiting Confirmation" }, { value: "ALL", label: "All" }, ]; const fmtDate = (iso?: string | null) => iso ? new Date(iso).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" }) : "—"; export function LastMileRequestsPanel() { const { toast } = useToast(); const qc = useQueryClient(); const { user } = useAuth(); const canApprove = hasPermission(user, FREIGHT_PERMS.lastMile.requestApprove); const [statusFilter, setStatusFilter] = useState("SUBMITTED"); const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [approveTarget, setApproveTarget] = useState(null); const [rejectTarget, setRejectTarget] = useState(null); const [advanceAmount, setAdvanceAmount] = useState(""); const [rejectReason, setRejectReason] = useState(""); const filter = { ...(statusFilter !== "ALL" ? { status: statusFilter } : {}), page: pagination.pageIndex + 1, pageSize: pagination.pageSize, }; const { data, isLoading } = useQuery({ queryKey: QUERY_KEYS.LAST_MILE_REQUESTS.list(filter), queryFn: async () => (await lastMileRequestsService.list(filter)).data, }); const rows = data?.data ?? []; const meta = data?.meta; const { data: freeTrucks } = useQuery({ queryKey: QUERY_KEYS.LAST_MILE_REQUESTS.freeTruckCount, queryFn: async () => (await lastMileRequestsService.freeTruckCount()).data, }); // Rule-based estimate for the approve dialog (estimated km × live last-mile // rates). Prefills the advance once, without clobbering a typed value. const { data: estimate } = useQuery({ queryKey: QUERY_KEYS.LAST_MILE_REQUESTS.priceEstimate(approveTarget?.id ?? ""), queryFn: async () => (await lastMileRequestsService.priceEstimate(approveTarget!.id)).data, enabled: Boolean(approveTarget), }); useEffect(() => { if (approveTarget && estimate?.total != null && advanceAmount === "") { setAdvanceAmount(estimate.total); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [estimate, approveTarget]); const invalidate = () => qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE_REQUESTS.ROOT }); const downloadContract = async (r: LastMileRequest) => { try { const { data: blob } = await lastMileRequestsService.contractDocument(r.id); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `LM_${r.booking?.company?.name?.replace(/[^A-Za-z0-9._-]+/g, "_") ?? r.id}.pdf`; a.click(); URL.revokeObjectURL(url); } catch { toast({ title: "Contract PDF not available", variant: "destructive" }); } }; const approve = useMutation({ mutationFn: () => lastMileRequestsService.approve(approveTarget!.id, Number(advanceAmount)), onSuccess: () => { void invalidate(); toast({ title: "Request approved" }); setApproveTarget(null); setAdvanceAmount(""); }, onError: (e: unknown) => { const description = (e as { response?: { data?: { message?: string } } })?.response?.data?.message; toast({ title: "Approve failed", description, variant: "destructive" }); }, }); const reject = useMutation({ mutationFn: () => lastMileRequestsService.reject(rejectTarget!.id, rejectReason.trim()), onSuccess: () => { void invalidate(); toast({ title: "Request rejected" }); setRejectTarget(null); setRejectReason(""); }, onError: (e: unknown) => { const description = (e as { response?: { data?: { message?: string } } })?.response?.data?.message; toast({ title: "Reject failed", description, variant: "destructive" }); }, }); const columns: ColumnDef[] = [ { id: "booking", header: () => Booking, cell: ({ row }) => { const r = row.original; return ( {r.booking?.reference ?? r.bookingId} {r.booking?.company?.name ?? "—"} ); }, }, { id: "containers", header: () => Requested Containers, cell: ({ row }) => { const r = row.original; const nums = r.requestedContainerNumbers; return ( {nums?.length ? nums.join(", ") : "—"} {r.requestedDeliveryDate && ( Delivery: {r.requestedDeliveryDate} )} ); }, }, { id: "submittedAt", header: () => Submitted, cell: ({ row }) => {fmtDate(row.original.submittedAt)}, }, { id: "status", header: () => Status, cell: ({ row }) => { const meta = STATUS_META[row.original.status]; return ( {meta.label} ); }, }, { id: "contract", header: () => LM Contract, cell: ({ row }) => { const r = row.original; if (r.status !== "APPROVED") return ; return ( {r.customerSignedAt ? "Signed" : "Awaiting signature"} ); }, }, ...(canApprove ? [ { id: "actions", header: () => Actions, cell: ({ row }: { row: { original: LastMileRequest } }) => { const r = row.original; if (r.status !== "SUBMITTED") return null; return ( ); }, } as ColumnDef, ] : []), ]; return ( {freeTrucks?.count ?? 0} truck{freeTrucks?.count === 1 ? "" : "s"} currently free {FILTER_OPTIONS.map((option) => { const active = statusFilter === option.value; return ( ); })} ( )} /> { setApproveTarget(null); setAdvanceAmount(""); }} title={Approve request{approveTarget?.booking?.reference ? ` · ${approveTarget.booking.reference}` : ""}} centered > {estimate?.total != null && ( {estimate.lines.map((line) => ( {line.description} — {line.amount.toLocaleString()} ))} Estimated total: {estimate.total.toLocaleString()} {estimate.currency} {estimate.estimatedKm != null ? ` · ${estimate.estimatedKm} km (straight-line estimate)` : ""} )} setRejectTarget(null)} title={Reject request{rejectTarget?.booking?.reference ? ` · ${rejectTarget.booking.reference}` : ""}} centered >