add clearance history and operations history endpoints, update UI components for history view

This commit is contained in:
Marshal
2026-06-28 19:08:30 +00:00
parent 258f1eb45a
commit 7af2ecc76e
8 changed files with 154 additions and 38 deletions

View File

@@ -506,4 +506,28 @@ export class ContractClearanceService {
sortOrder: filter.sortOrder, sortOrder: filter.sortOrder,
}); });
} }
/** GL ET history: contracts that completed Path B clearance. */
async history(filter: FilterContractDto): Promise<PaginatedContracts> {
return this.contractsRepository.findAllPaginated({
page: filter.page ?? 1,
pageSize: filter.pageSize ?? 50,
statuses: ['CLEARANCE_READY_FOR_BOOKING', 'ACTIVE', 'CLOSED', 'CANCELLED'],
customsClearingEnabled: true,
sortBy: filter.sortBy ?? 'createdAt',
sortOrder: filter.sortOrder ?? 'DESC',
});
}
/** Operations history: contracts that completed Path A self-clearance review. */
async opsHistory(filter: FilterContractDto): Promise<PaginatedContracts> {
return this.contractsRepository.findAllPaginated({
page: filter.page ?? 1,
pageSize: filter.pageSize ?? 50,
statuses: ['CLEARANCE_READY_FOR_BOOKING', 'ACTIVE', 'CLOSED', 'CANCELLED'],
customsClearingEnabled: false,
sortBy: filter.sortBy ?? 'createdAt',
sortOrder: filter.sortOrder ?? 'DESC',
});
}
} }

View File

@@ -459,6 +459,20 @@ export class ContractsController {
return this.clearanceService.opsFinalize(id); return this.clearanceService.opsFinalize(id);
} }
@Get('clearance/history')
@BookingStaff(FREIGHT_PERMS.contracts.finalizeClearance)
@ApiOperation({ summary: 'GL ET clearance history: contracts that completed Path B clearance' })
clearanceHistory(@Query() filter: FilterContractDto) {
return this.clearanceService.history(filter);
}
@Get('clearance/ops-history')
@BookingStaff(FREIGHT_PERMS.contracts.opsClearanceReview)
@ApiOperation({ summary: 'Operations clearance history: contracts that completed Path A self-clearance' })
opsClearanceHistory(@Query() filter: FilterContractDto) {
return this.clearanceService.opsHistory(filter);
}
// ── Booking under contract (Path A customer / Path B GL ET) ──────────────── // ── Booking under contract (Path A customer / Path B GL ET) ────────────────
@Post(':id/bookings') @Post(':id/bookings')

View File

@@ -112,9 +112,7 @@ export function ContractClearanceReviewSection({
); );
const glDocs = useMemo( const glDocs = useMemo(
() => () =>
(clearance?.documents ?? []).filter( (clearance?.documents ?? []).filter((d) => d.uploadedBy === "gl"),
(d) => d.uploadedBy === "gl_et" || d.uploadedBy === "gl_dj",
),
[clearance], [clearance],
); );

View File

@@ -56,6 +56,8 @@ export const QUERY_KEYS = {
clearance: (id: string) => ["contracts", "clearance", id] as const, clearance: (id: string) => ["contracts", "clearance", id] as const,
clearanceQueue: (region?: string) => clearanceQueue: (region?: string) =>
["contracts", "clearance-queue", region ?? "ET"] as const, ["contracts", "clearance-queue", region ?? "ET"] as const,
clearanceHistory: (region?: string) =>
["contracts", "clearance-history", region ?? "ET"] as const,
milestones: (id: string) => ["contracts", "milestones", id] as const, milestones: (id: string) => ["contracts", "milestones", id] as const,
capacity: (id: string) => ["contracts", "capacity", id] as const, capacity: (id: string) => ["contracts", "capacity", id] as const,
bookingMilestones: (bookingId: string) => bookingMilestones: (bookingId: string) =>

View File

@@ -150,6 +150,8 @@ export const URL_CONSTANTS = {
`/contracts/${id}/clearance/ops-review`, `/contracts/${id}/clearance/ops-review`,
OPS_CLEARANCE_FINALIZE: (id: string) => OPS_CLEARANCE_FINALIZE: (id: string) =>
`/contracts/${id}/clearance/ops-finalize`, `/contracts/${id}/clearance/ops-finalize`,
CLEARANCE_HISTORY: "/contracts/clearance/history",
OPS_CLEARANCE_HISTORY: "/contracts/clearance/ops-history",
BOOKINGS: (id: string) => `/contracts/${id}/bookings`, BOOKINGS: (id: string) => `/contracts/${id}/bookings`,
CAPACITY: (id: string) => `/contracts/${id}/capacity`, CAPACITY: (id: string) => `/contracts/${id}/capacity`,
MILESTONES: (id: string) => `/contracts/${id}/milestones`, MILESTONES: (id: string) => `/contracts/${id}/milestones`,

View File

@@ -61,6 +61,22 @@ export function useOpsClearanceQueue(enabled = true) {
}); });
} }
export function useContractClearanceHistory(enabled = true) {
return useQuery({
queryKey: QUERY_KEYS.CONTRACTS.clearanceHistory("GL"),
queryFn: () => contractsService.getClearanceHistory(),
enabled,
});
}
export function useOpsClearanceHistory(enabled = true) {
return useQuery({
queryKey: QUERY_KEYS.CONTRACTS.clearanceHistory("OPS"),
queryFn: () => contractsService.getOpsClearanceHistory(),
enabled,
});
}
export function useContractMilestones(id: string | undefined) { export function useContractMilestones(id: string | undefined) {
return useQuery({ return useQuery({
queryKey: QUERY_KEYS.CONTRACTS.milestones(id ?? ""), queryKey: QUERY_KEYS.CONTRACTS.milestones(id ?? ""),

View File

@@ -15,8 +15,10 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { import {
ArrowRight, ArrowRight,
CheckCircle,
ChevronRight, ChevronRight,
FileText, FileText,
History,
Inbox, Inbox,
LayoutGrid, LayoutGrid,
RefreshCw, RefreshCw,
@@ -36,15 +38,20 @@ import {
} from "@edr/ui-common"; } from "@edr/ui-common";
import type { Freight } from "@edr/types"; import type { Freight } from "@edr/types";
import { ContractStatusBadge } from "@/components/contracts/ContractStatusBadge";
import { PageContainer } from "@/components/page/PageContainer"; import { PageContainer } from "@/components/page/PageContainer";
import { PageHeader } from "@/components/page/PageHeader"; import { PageHeader } from "@/components/page/PageHeader";
import { KpiStrip } from "@/components/page/KpiStrip"; import { KpiStrip } from "@/components/page/KpiStrip";
import { bookingTable } from "@/components/bookings/booking-ui.styles"; import { bookingTable } from "@/components/bookings/booking-ui.styles";
import { import {
useContractClearanceHistory,
useContractClearanceQueue, useContractClearanceQueue,
useOpsClearanceHistory,
useOpsClearanceQueue, useOpsClearanceQueue,
} from "@/hooks/contracts/useContracts"; } from "@/hooks/contracts/useContracts";
type PageTab = "queue" | "history";
type ViewMode = "table" | "cards"; type ViewMode = "table" | "cards";
interface ClearanceRow { interface ClearanceRow {
@@ -56,6 +63,7 @@ interface ClearanceRow {
originLabel: string; originLabel: string;
destinationLabel: string; destinationLabel: string;
contractKind: string; contractKind: string;
status: string;
} }
function yardLabel( function yardLabel(
@@ -83,6 +91,7 @@ function toClearanceRow(contract: Freight.IContract): ClearanceRow {
originLabel: yardLabel(first?.originYard), originLabel: yardLabel(first?.originYard),
destinationLabel: yardLabel(last?.destinationYard), destinationLabel: yardLabel(last?.destinationYard),
contractKind: contract.contractKind, contractKind: contract.contractKind,
status: contract.status,
}; };
} }
@@ -119,13 +128,19 @@ export default function ContractClearanceListPage({
const navigate = useNavigate(); const navigate = useNavigate();
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [view, setView] = useState<ViewMode>("table"); const [view, setView] = useState<ViewMode>("table");
const [pageTab, setPageTab] = useState<PageTab>("queue");
const { pagination, setPagination } = usePagination({ pageSize: 10 }); const { pagination, setPagination } = usePagination({ pageSize: 10 });
const glQueue = useContractClearanceQueue(!opsMode); const isHistory = pageTab === "history";
const opsQueue = useOpsClearanceQueue(opsMode);
const glQueue = useContractClearanceQueue(!opsMode && !isHistory);
const opsQueue = useOpsClearanceQueue(opsMode && !isHistory);
const glHistory = useContractClearanceHistory(!opsMode && isHistory);
const opsHistory = useOpsClearanceHistory(opsMode && isHistory);
const { data, isLoading, isError, isFetching, refetch } = opsMode const { data, isLoading, isError, isFetching, refetch } = opsMode
? opsQueue ? (isHistory ? opsHistory : opsQueue)
: glQueue; : (isHistory ? glHistory : glQueue);
const allRows = useMemo( const allRows = useMemo(
() => (data?.items ?? []).map(toClearanceRow), () => (data?.items ?? []).map(toClearanceRow),
@@ -229,11 +244,14 @@ export default function ContractClearanceListPage({
{ {
id: "status", id: "status",
header: () => <span className={bookingTable.headerCell}>Status</span>, header: () => <span className={bookingTable.headerCell}>Status</span>,
cell: () => ( cell: ({ row }) =>
<Badge size="sm" variant="light" color="edr-green" radius="sm"> isHistory ? (
Under review <ContractStatusBadge status={row.original.status} />
</Badge> ) : (
), <Badge size="sm" variant="light" color="edr-green" radius="sm">
Under review
</Badge>
),
}, },
{ {
id: "go", id: "go",
@@ -261,11 +279,11 @@ export default function ContractClearanceListPage({
meta={ meta={
<Badge <Badge
variant="light" variant="light"
color="edr-green" color={isHistory ? "gray" : "edr-green"}
radius="sm" radius="sm"
leftSection={<ShieldCheck size={13} />} leftSection={isHistory ? <History size={13} /> : <ShieldCheck size={13} />}
> >
{counts.all} awaiting review {isHistory ? `${counts.all} completed` : `${counts.all} awaiting review`}
</Badge> </Badge>
} }
action={ action={
@@ -282,28 +300,53 @@ export default function ContractClearanceListPage({
} }
/> />
<Group>
<SegmentedControl
size="sm"
radius="md"
value={pageTab}
onChange={(v) => {
setPageTab(v as PageTab);
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
}}
data={[
{
value: "queue",
label: (
<Group gap={6} wrap="nowrap">
<Inbox size={15} />
<Box visibleFrom="xs">Queue</Box>
</Group>
),
},
{
value: "history",
label: (
<Group gap={6} wrap="nowrap">
<History size={15} />
<Box visibleFrom="xs">History</Box>
</Group>
),
},
]}
/>
</Group>
<KpiStrip <KpiStrip
loading={isLoading} loading={isLoading}
items={[ items={
{ isHistory
label: "Awaiting review", ? [
value: counts.all, { label: "Total cleared", value: counts.all, icon: CheckCircle, color: "edr-green" },
icon: Inbox, { label: "Import", value: counts.import, icon: Truck, color: "edr-green" },
color: "edr-green", { label: "Export", value: counts.export, icon: ShipWheel, color: "gray" },
}, ]
{ : [
label: "Import", { label: "Awaiting review", value: counts.all, icon: Inbox, color: "edr-green" },
value: counts.import, { label: "Import", value: counts.import, icon: Truck, color: "edr-green" },
icon: Truck, { label: "Export", value: counts.export, icon: ShipWheel, color: "gray" },
color: "edr-green", ]
}, }
{
label: "Export",
value: counts.export,
icon: ShipWheel,
color: "gray",
},
]}
/> />
<Card p={0} withBorder shadow="sm" radius="lg"> <Card p={0} withBorder shadow="sm" radius="lg">
@@ -401,6 +444,7 @@ export default function ContractClearanceListPage({
rows={pagedRows} rows={pagedRows}
loading={isLoading} loading={isLoading}
onOpen={openDetail} onOpen={openDetail}
isHistory={isHistory}
/> />
)} )}
</Stack> </Stack>
@@ -414,10 +458,12 @@ function ClearanceCardGrid({
rows, rows,
loading, loading,
onOpen, onOpen,
isHistory = false,
}: { }: {
rows: ClearanceRow[]; rows: ClearanceRow[];
loading: boolean; loading: boolean;
onOpen: (id: string) => void; onOpen: (id: string) => void;
isHistory?: boolean;
}) { }) {
if (loading) { if (loading) {
return ( return (
@@ -449,7 +495,7 @@ function ClearanceCardGrid({
}} }}
> >
{rows.map((r) => ( {rows.map((r) => (
<ClearanceCard key={r.id} row={r} onOpen={() => onOpen(r.id)} /> <ClearanceCard key={r.id} row={r} onOpen={() => onOpen(r.id)} isHistory={isHistory} />
))} ))}
</Box> </Box>
); );
@@ -458,9 +504,11 @@ function ClearanceCardGrid({
function ClearanceCard({ function ClearanceCard({
row, row,
onOpen, onOpen,
isHistory = false,
}: { }: {
row: ClearanceRow; row: ClearanceRow;
onOpen: () => void; onOpen: () => void;
isHistory?: boolean;
}) { }) {
return ( return (
<Card <Card
@@ -497,8 +545,8 @@ function ClearanceCard({
</Group> </Group>
</Box> </Box>
</Group> </Group>
<Badge size="sm" variant="light" color="edr-green" radius="sm"> <Badge size="sm" variant="light" color={isHistory ? "gray" : "edr-green"} radius="sm">
Under review {isHistory ? "Cleared" : "Under review"}
</Badge> </Badge>
</Group> </Group>

View File

@@ -212,6 +212,18 @@ export const contractsService = {
}; };
}, },
getClearanceHistory: async (): Promise<PaginatedContracts> => {
const response = await client.get<PaginatedContracts>(C.CLEARANCE_HISTORY);
const data = unwrap(response.data);
return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0 };
},
getOpsClearanceHistory: async (): Promise<PaginatedContracts> => {
const response = await client.get<PaginatedContracts>(C.OPS_CLEARANCE_HISTORY);
const data = unwrap(response.data);
return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0 };
},
opsReviewClearanceDocument: ( opsReviewClearanceDocument: (
id: string, id: string,
payload: { fileKey: string; status: "APPROVED" | "QUERIED"; note?: string }, payload: { fileKey: string; status: "APPROVED" | "QUERIED"; note?: string },