mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 07:08:18 +00:00
- Create migration for booking_requests table with necessary fields and indexes. - Implement BookingRequestRepository for database operations related to booking requests. - Develop BookingRequestService to handle business logic for submitting, accepting, rejecting, and canceling booking requests. - Create DTOs for creating booking requests and reviewing them. - Define BookingRequest entity to map to the booking_requests table. - Add UI components for managing shipment requests, including detail and list pages. - Implement OperationDatePicker component for selecting available shipment days.
209 lines
5.7 KiB
TypeScript
209 lines
5.7 KiB
TypeScript
import { useMemo, useState } from "react";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import {
|
||
ActionIcon,
|
||
Badge,
|
||
Box,
|
||
Group,
|
||
Stack,
|
||
Text,
|
||
TextInput,
|
||
} from "@mantine/core";
|
||
import { ChevronRight, 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 { 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))
|
||
: "—";
|
||
|
||
/** Summarize requested quantities for the list row. */
|
||
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 "—";
|
||
}
|
||
|
||
interface RequestRow {
|
||
id: string;
|
||
reference: string;
|
||
contractReference: string;
|
||
scheduledDate?: string | null;
|
||
summary: string;
|
||
}
|
||
|
||
export default function ShipmentRequestsPage() {
|
||
const navigate = useNavigate();
|
||
const [query, setQuery] = useState("");
|
||
|
||
const { data, isLoading, isError, isFetching, refetch } = useQuery({
|
||
queryKey: ["shipment-request-queue"],
|
||
queryFn: () => contractsService.getBookingRequestQueue(),
|
||
refetchInterval: 30_000,
|
||
});
|
||
|
||
const rows = useMemo<RequestRow[]>(() => {
|
||
const all = (data ?? []).map((r) => ({
|
||
id: r.id,
|
||
reference: r.reference || r.id.slice(0, 8),
|
||
contractReference: r.contract?.reference ?? r.contractId,
|
||
scheduledDate: r.scheduledDate,
|
||
summary: summarizeLines(r.requestedLines ?? {}),
|
||
}));
|
||
const q = query.trim().toLowerCase();
|
||
if (!q) return all;
|
||
return all.filter(
|
||
(r) =>
|
||
r.reference.toLowerCase().includes(q) ||
|
||
r.contractReference.toLowerCase().includes(q) ||
|
||
r.summary.toLowerCase().includes(q),
|
||
);
|
||
}, [data, query]);
|
||
|
||
const columns = useMemo<ColumnDef<RequestRow>[]>(
|
||
() => [
|
||
{
|
||
id: "reference",
|
||
header: "Request",
|
||
meta: cellMeta,
|
||
cell: ({ row }) => (
|
||
<Text size="sm" fw={700} c="dark.5">
|
||
{row.original.reference}
|
||
</Text>
|
||
),
|
||
},
|
||
{
|
||
id: "contract",
|
||
header: "Contract",
|
||
meta: cellMeta,
|
||
cell: ({ row }) => (
|
||
<Text size="sm" c="gray.7">
|
||
{row.original.contractReference}
|
||
</Text>
|
||
),
|
||
},
|
||
{
|
||
id: "summary",
|
||
header: "Requested",
|
||
meta: cellMeta,
|
||
cell: ({ row }) => (
|
||
<Badge variant="light" color="edr-green" radius="sm">
|
||
{row.original.summary}
|
||
</Badge>
|
||
),
|
||
},
|
||
{
|
||
id: "date",
|
||
header: "Preferred date",
|
||
meta: cellMeta,
|
||
cell: ({ row }) => (
|
||
<Text size="sm">{fmtDate(row.original.scheduledDate)}</Text>
|
||
),
|
||
},
|
||
{
|
||
id: "go",
|
||
size: 56,
|
||
cell: () => (
|
||
<Group justify="flex-end" pr="xs">
|
||
<ChevronRight size={16} className="text-muted-foreground" />
|
||
</Group>
|
||
),
|
||
},
|
||
],
|
||
[],
|
||
);
|
||
|
||
return (
|
||
<PageContainer>
|
||
<Stack gap="lg">
|
||
<PageHeader
|
||
title="Shipment Requests"
|
||
subtitle="Customer requests to ship under general customs contracts. Accept one to create the booking and start its clearance."
|
||
meta={
|
||
<Badge
|
||
variant="light"
|
||
color="edr-green"
|
||
radius="sm"
|
||
leftSection={<PackageSearch size={13} />}
|
||
>
|
||
{rows.length} pending
|
||
</Badge>
|
||
}
|
||
action={
|
||
<ActionIcon
|
||
variant="default"
|
||
size="lg"
|
||
radius="md"
|
||
onClick={() => refetch()}
|
||
loading={isFetching}
|
||
aria-label="Refresh"
|
||
>
|
||
<RefreshCw size={16} />
|
||
</ActionIcon>
|
||
}
|
||
/>
|
||
|
||
<TextInput
|
||
radius="md"
|
||
maw={360}
|
||
placeholder="Search request, contract, cargo…"
|
||
leftSection={<Search size={15} />}
|
||
value={query}
|
||
onChange={(e) => setQuery(e.currentTarget.value)}
|
||
/>
|
||
|
||
{rows.length === 0 && !isLoading ? (
|
||
<Box
|
||
py={56}
|
||
style={{
|
||
borderRadius: 14,
|
||
border: "1px dashed var(--mantine-color-gray-3)",
|
||
textAlign: "center",
|
||
}}
|
||
>
|
||
<Inbox size={26} className="text-muted-foreground" />
|
||
<Text c="dimmed" mt="sm">
|
||
No pending shipment requests.
|
||
</Text>
|
||
</Box>
|
||
) : (
|
||
<DataTable
|
||
columns={columns}
|
||
data={rows}
|
||
status={isLoading ? "loading" : isError ? "error" : "success"}
|
||
onRowClick={(row) =>
|
||
navigate(`/dashboard/shipment-requests/${row.id}`)
|
||
}
|
||
containerClassName="overflow-x-auto rounded-lg border border-edr-border"
|
||
/>
|
||
)}
|
||
</Stack>
|
||
</PageContainer>
|
||
);
|
||
}
|