import { useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ActionIcon, Badge, Box, Button, CloseButton, Group, Modal, Paper, SegmentedControl, Select, Stack, Text, Textarea, TextInput, } from "@mantine/core"; import { DatePickerInput } from "@mantine/dates"; import { getDateRangePresets } from "@/components/common/dateRangePresets"; import { ArrowUpDown, FilterX, Inbox, PackageSearch, RefreshCw, Search, } from "lucide-react"; import { DataTable, type ColumnDef } from "@edr/ui-common"; import type { Freight } from "@edr/types"; import { PageContainer } from "@/components/page/PageContainer"; import { PageHeader } from "@/components/page/PageHeader"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { getShipmentRejectAction, getShipmentStaffRowAction, type ShipmentListRow, } from "@/features/contracts/mapShipmentListRow"; import { contractsService } from "@/services/contracts.service"; const cellMeta = { headerClassName: ruleEngineTable.headerCell, cellClassName: ruleEngineTable.bodyCell, }; const fmtDate = (iso?: string | null) => iso ? new Intl.DateTimeFormat("en-GB", { day: "2-digit", month: "short", year: "numeric", }).format(new Date(iso)) : "—"; const fmtDateTime = (iso?: string | null) => iso ? new Intl.DateTimeFormat("en-GB", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", }).format(new Date(iso)) : "—"; function summarizeLines(lines: Freight.RequestedShipmentLines): string { if (lines.containers?.length) { return lines.containers .map((c) => `${c.quantity}× ${c.containerSize}`) .join(", "); } if (lines.bulk) { const b = lines.bulk; if (b.cargoWeightTons) return `${b.cargoWeightTons} t bulk`; if (b.itemCount) return `${b.itemCount} items`; return "Bulk"; } return "—"; } const STATUS_META: Record< Freight.BookingRequestStatus, { label: string; color: string } > = { PENDING: { label: "Pending", color: "yellow" }, ACCEPTED: { label: "Accepted", color: "edr-green" }, REJECTED: { label: "Rejected", color: "red" }, CANCELLED: { label: "Cancelled", color: "gray" }, }; type StatusFilter = "ALL" | Freight.BookingRequestStatus; type CargoFilter = "ALL" | "CONTAINER" | "BULK"; type SortKey = | "submitted-desc" | "submitted-asc" | "preferred-asc" | "preferred-desc" | "reference"; const SORT_OPTIONS: Array<{ value: SortKey; label: string }> = [ { value: "submitted-desc", label: "Newest first" }, { value: "submitted-asc", label: "Oldest first" }, { value: "preferred-asc", label: "Preferred date (soonest)" }, { value: "preferred-desc", label: "Preferred date (latest)" }, { value: "reference", label: "Reference A–Z" }, ]; const time = (iso?: string | null) => (iso ? new Date(iso).getTime() : 0); export default function ShipmentRequestsPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const [query, setQuery] = useState(""); const [status, setStatus] = useState("PENDING"); const [cargo, setCargo] = useState("ALL"); const [preferredFrom, setPreferredFrom] = useState(null); const [preferredTo, setPreferredTo] = useState(null); const [sort, setSort] = useState("submitted-desc"); const [rejectTarget, setRejectTarget] = useState(null); const [rejectNote, setRejectNote] = useState(""); const [acceptTarget, setAcceptTarget] = useState(null); const { data, isLoading, isError, isFetching, refetch } = useQuery({ queryKey: ["shipment-request-queue"], queryFn: () => contractsService.getBookingRequestQueue(), refetchInterval: 30_000, }); const reject = useMutation({ mutationFn: () => contractsService.rejectBookingRequest(rejectTarget!.id, rejectNote), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["shipment-request-queue"] }); setRejectTarget(null); setRejectNote(""); }, }); const allRows = useMemo( () => (data ?? []).map((r) => { const lines = r.requestedLines ?? {}; return { id: r.id, reference: r.reference || r.id.slice(0, 8), contractId: r.contractId, contractReference: r.contract?.reference ?? r.contractId, scheduledDate: r.scheduledDate, summary: summarizeLines(lines), status: r.status, createdBookingId: r.createdBookingId, createdAt: r.createdAt, customerName: r.contract?.company?.name ?? null, freightKind: lines.containers?.length ? "CONTAINER" : lines.bulk ? "BULK" : r.contract?.freightType === "BULK" ? "BULK" : "CONTAINER", hazardous: (lines.containers ?? []).some((c) => (c.hazardousQuantity ?? 0) > 0) || (lines.bulk?.hazardousQuantity ?? 0) > 0, reefer: (lines.containers ?? []).some( (c) => (c.reeferQuantity ?? 0) > 0, ), }; }), [data], ); // Status counts always reflect the whole queue so the segmented control // reads as a live overview, independent of the other filters. const counts = useMemo(() => { const c: Record = { ALL: allRows.length, PENDING: 0, ACCEPTED: 0, REJECTED: 0, CANCELLED: 0, }; allRows.forEach((r) => { c[r.status] += 1; }); return c; }, [allRows]); const rows = useMemo(() => { let out = allRows; if (status !== "ALL") out = out.filter((r) => r.status === status); if (cargo !== "ALL") out = out.filter((r) => r.freightKind === cargo); // Preferred-date range: rows without a preferred day drop out once a bound // is set — a date filter that keeps dateless rows reads as broken. if (preferredFrom || preferredTo) { const from = preferredFrom ? preferredFrom.getTime() : -Infinity; const to = preferredTo ? preferredTo.getTime() + 24 * 60 * 60 * 1000 - 1 : Infinity; out = out.filter((r) => { if (!r.scheduledDate) return false; const t = time(r.scheduledDate); return t >= from && t <= to; }); } const q = query.trim().toLowerCase(); if (q) { out = out.filter( (r) => r.reference.toLowerCase().includes(q) || r.contractReference.toLowerCase().includes(q) || (r.customerName ?? "").toLowerCase().includes(q) || r.summary.toLowerCase().includes(q), ); } const sorted = [...out]; switch (sort) { case "submitted-asc": sorted.sort((a, b) => time(a.createdAt) - time(b.createdAt)); break; case "preferred-asc": // Requests without a preferred day sink to the bottom in both orders. sorted.sort( (a, b) => (a.scheduledDate ? time(a.scheduledDate) : Infinity) - (b.scheduledDate ? time(b.scheduledDate) : Infinity), ); break; case "preferred-desc": sorted.sort( (a, b) => (b.scheduledDate ? time(b.scheduledDate) : -Infinity) - (a.scheduledDate ? time(a.scheduledDate) : -Infinity), ); break; case "reference": sorted.sort((a, b) => a.reference.localeCompare(b.reference)); break; default: // Newest submitted on top. sorted.sort((a, b) => time(b.createdAt) - time(a.createdAt)); } return sorted; }, [allRows, status, cargo, preferredFrom, preferredTo, query, sort]); const filtersActive = query.trim() !== "" || status !== "PENDING" || cargo !== "ALL" || preferredFrom !== null || preferredTo !== null || sort !== "submitted-desc"; const clearFilters = () => { setQuery(""); setStatus("PENDING"); setCargo("ALL"); setPreferredFrom(null); setPreferredTo(null); setSort("submitted-desc"); }; const columns = useMemo[]>( () => [ { id: "reference", header: "Request", meta: cellMeta, cell: ({ row }) => ( {row.original.reference} Submitted {fmtDateTime(row.original.createdAt)} ), }, { id: "contract", header: "Contract", meta: cellMeta, cell: ({ row }) => ( {row.original.contractReference} {row.original.customerName ? ( {row.original.customerName} ) : null} ), }, { id: "summary", header: "Requested", meta: cellMeta, cell: ({ row }) => ( {row.original.summary} {row.original.hazardous ? ( Hazardous ) : null} {row.original.reefer ? ( Reefer ) : null} ), }, { id: "date", header: "Preferred date", meta: cellMeta, cell: ({ row }) => ( {fmtDate(row.original.scheduledDate)} ), }, { id: "status", header: "Status", meta: cellMeta, cell: ({ row }) => { const meta = STATUS_META[row.original.status]; return ( {meta.label} ); }, }, { id: "actions", header: () => Action, meta: cellMeta, cell: ({ row }) => { const primary = getShipmentStaffRowAction(row.original); const rejectAction = getShipmentRejectAction(row.original); return ( {rejectAction ? ( ) : null} {primary.kind === "navigate" ? ( ) : null} ); }, }, ], [navigate], ); const hasAnyRequests = allRows.length > 0; return ( } > {counts.PENDING} pending } action={ refetch()} loading={isFetching} aria-label="Refresh" > } /> } rightSection={ query ? ( setQuery("")} /> ) : null } value={query} onChange={(e) => setQuery(e.currentTarget.value)} /> } value={sort} onChange={(v) => setSort((v as SortKey) ?? "submitted-desc")} data={SORT_OPTIONS} allowDeselect={false} aria-label="Sort by" /> setStatus(v as StatusFilter)} data={[ { value: "ALL", label: `All · ${counts.ALL}` }, { value: "PENDING", label: `Pending · ${counts.PENDING}` }, { value: "ACCEPTED", label: `Accepted · ${counts.ACCEPTED}` }, { value: "REJECTED", label: `Rejected · ${counts.REJECTED}` }, { value: "CANCELLED", label: `Cancelled · ${counts.CANCELLED}` }, ]} /> {rows.length} of {allRows.length} request {allRows.length === 1 ? "" : "s"} {filtersActive ? ( ) : null} {rows.length === 0 && !isLoading && !isError ? ( {hasAnyRequests ? ( <> No requests match the current filters. ) : ( No shipment requests yet. )} ) : ( navigate(`/dashboard/shipment-requests/${row.id}`) } containerClassName="overflow-x-auto rounded-lg border border-edr-border" /> )} { if (!reject.isPending) { setRejectTarget(null); setRejectNote(""); } }} title="Reject shipment request" radius="md" centered > Reject request{" "} {rejectTarget?.reference} ? The customer will be notified.