Merge branch 'freight_feature/contrat' of github.com:Tria-plc/edr-platform into freight_feature/contrat

This commit is contained in:
marshal
2026-06-28 22:09:17 +03:00
8 changed files with 154 additions and 38 deletions

View File

@@ -506,4 +506,28 @@ export class ContractClearanceService {
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);
}
@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) ────────────────
@Post(':id/bookings')

View File

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

View File

@@ -56,6 +56,8 @@ export const QUERY_KEYS = {
clearance: (id: string) => ["contracts", "clearance", id] as const,
clearanceQueue: (region?: string) =>
["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,
capacity: (id: string) => ["contracts", "capacity", id] as const,
bookingMilestones: (bookingId: string) =>

View File

@@ -150,6 +150,8 @@ export const URL_CONSTANTS = {
`/contracts/${id}/clearance/ops-review`,
OPS_CLEARANCE_FINALIZE: (id: string) =>
`/contracts/${id}/clearance/ops-finalize`,
CLEARANCE_HISTORY: "/contracts/clearance/history",
OPS_CLEARANCE_HISTORY: "/contracts/clearance/ops-history",
BOOKINGS: (id: string) => `/contracts/${id}/bookings`,
CAPACITY: (id: string) => `/contracts/${id}/capacity`,
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) {
return useQuery({
queryKey: QUERY_KEYS.CONTRACTS.milestones(id ?? ""),

View File

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