mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 13:40:57 +00:00
add clearance history and operations history endpoints, update UI components for history view
This commit is contained in:
@@ -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',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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`,
|
||||||
|
|||||||
@@ -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 ?? ""),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
Reference in New Issue
Block a user