add e2e test

This commit is contained in:
Marshal
2026-07-22 23:44:32 +00:00
parent b0f561a935
commit 2481f43f1f
32 changed files with 4702 additions and 125 deletions

View File

@@ -581,6 +581,16 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
const ET_CLEARANCE_HREF = "/dashboard/contracts/clearance";
const DJ_CLEARANCE_HREF = "/dashboard/gl-djibouti/clearance";
// Routes a GL officer may reach beyond their clearance hub. Path B booking is
// part of their job (create/rebook under a cleared contract, then view that
// booking's clearance), but those routes live outside the clearance prefix —
// without this allowlist the single-prefix lock bounces them out of their own
// workflow. Matched against location.pathname (no query string).
const GL_WORKFLOW_PATH_PATTERNS: RegExp[] = [
/^\/dashboard\/contracts\/[^/]+\/create-booking(\/|$)/,
/^\/dashboard\/bookings\/[^/]+\/clearance(\/|$)/,
];
const isEtClearanceItem = (item: SidebarItem): boolean =>
item.href === ET_CLEARANCE_HREF;
const isDjClearanceItem = (item: SidebarItem): boolean =>
@@ -714,7 +724,11 @@ const DashboardShell = () => {
document.title = activeLabel ? `${activeLabel} | ${APP_TITLE}` : APP_TITLE;
}, [location.pathname, sidebarSections]);
if (glClearanceHome && !location.pathname.startsWith(glClearanceHome)) {
if (
glClearanceHome &&
!location.pathname.startsWith(glClearanceHome) &&
!GL_WORKFLOW_PATH_PATTERNS.some((re) => re.test(location.pathname))
) {
return <Navigate to={glClearanceHome} replace />;
}

View File

@@ -6,7 +6,6 @@ import {
LayoutGrid,
Milestone,
Package,
ShieldCheck,
} from "lucide-react";
import {
Container,
@@ -36,7 +35,6 @@ import {
BookingCompanyCard,
BookingContractSummaryCard,
BookingContainerUnitsCard,
ClearanceReviewSection,
BookingDocumentsPanel,
ContractOrdersPanel,
} from "@/components/bookings/detail";
@@ -129,13 +127,9 @@ export default function BookingRequestDetailPage() {
const row = toBookingListRow(booking);
const statusMeta = getStatusMeta(booking.status);
// Non-customs clearance is reviewed here by Marketing in its own tab; customs
// bookings are handled in the Global Logistics clearance queue instead.
const showClearanceTab =
!booking.customsClearingEnabled &&
["AWAITING_DOCUMENTS", "DOCUMENTS_UNDER_REVIEW", "CLEARANCE_READY"].includes(
booking.status,
);
// Clearance review + finalize now lives solely on the Operations "Clearance
// Documents" hub (/dashboard/contracts/clearance-documents → detail page), so
// no clearance tab is embedded here anymore.
// A general contract drives an "Orders" tab: each drawdown order spawns a
// child booking that staff manage (clearance/approval) independently.
const isGeneralContract = booking.bookingType === "GENERAL_CONTRACT";
@@ -143,13 +137,11 @@ export default function BookingRequestDetailPage() {
// customs-workflow, invoice or notice files — so the tab bar always renders.
const requestedTab = searchParams.get("tab");
const activeTab =
requestedTab === "clearance" && showClearanceTab
? "clearance"
: requestedTab === "orders" && isGeneralContract
? "orders"
: requestedTab === "documents"
? "documents"
: "overview";
requestedTab === "orders" && isGeneralContract
? "orders"
: requestedTab === "documents"
? "documents"
: "overview";
const setActiveTab = (tab: string | null) => {
const next = new URLSearchParams(searchParams);
if (tab && tab !== "overview") next.set("tab", tab);
@@ -209,14 +201,6 @@ export default function BookingRequestDetailPage() {
Orders
</Tabs.Tab>
)}
{showClearanceTab && (
<Tabs.Tab
value="clearance"
leftSection={<ShieldCheck size={16} />}
>
Customer clearance
</Tabs.Tab>
)}
<Tabs.Tab
value="documents"
leftSection={<FolderOpen size={16} />}
@@ -236,14 +220,6 @@ export default function BookingRequestDetailPage() {
/>
</Tabs.Panel>
)}
{showClearanceTab && (
<Tabs.Panel value="clearance">
<ClearanceReviewSection
bookingId={booking.id}
onChanged={() => refetch()}
/>
</Tabs.Panel>
)}
<Tabs.Panel value="documents">
<BookingDocumentsPanel bookingId={booking.id} />
</Tabs.Panel>

View File

@@ -138,9 +138,14 @@ export default function ClearanceDocumentsPage() {
const generalQuery = useQuery({
queryKey: ["clearance-documents", "general", bookingStatuses, page, search],
queryFn: () =>
// Per-booking self-clearance instances are drawdowns under GENERAL
// non-customs contracts: they carry bookingType=ONE_TIME (each shipment
// is one-time) with contractKind=GENERAL, so filtering on
// bookingType=GENERAL_CONTRACT returned nothing. customsClearingEnabled
// =false + the three per-booking clearance statuses already isolate
// exactly this worklist — the same set the old booking-request tab showed.
bookingsService.list({
statuses: bookingStatuses,
bookingType: "GENERAL_CONTRACT",
customsClearingEnabled: "false",
page,
pageSize: PAGE_SIZE,

View File

@@ -93,6 +93,11 @@ export default function ContractClearanceDetailPage() {
}, [clearance]);
const reference = contract?.reference ?? "Clearance";
// Path A (non-customs) → Operations reviews & finalizes; Path B (customs) → GL.
// This page serves BOTH hubs (Ops "Clearance Documents" + GL "Document
// Clearance"), so the reviewer is decided by the contract, not the hub — a
// hardcoded value routes non-customs finalize to the GL endpoint and 409s.
const selfClear = !contract?.customsClearingEnabled;
const phasedCustoms =
contract?.contractKind === "ONE_TIME" && Boolean(contract.customsClearingEnabled);
const docsPhaseComplete =
@@ -338,7 +343,7 @@ export default function ContractClearanceDetailPage() {
<ContractClearanceReviewSection
contractId={id!}
hideSummary
selfClear={false}
selfClear={selfClear}
readOnly={reviewReadOnly}
approvalsLocked={phasedCustoms && docReviewLocked}
queriesLocked={queriesLocked}

View File

@@ -19,7 +19,6 @@ import {
Receipt,
RefreshCw,
Route as RouteIcon,
ShieldCheck,
Snowflake,
Users,
} from "lucide-react";
@@ -51,7 +50,6 @@ import { ContractStatusBadge } from "@/components/contracts/ContractStatusBadge"
import { ContractWorkflowStepper } from "@/components/contracts/ContractWorkflowStepper";
import { ContractActionsToolbar } from "@/components/contracts/ContractActionsToolbar";
import { ContractApprovalStepsCard } from "@/components/contracts/ContractApprovalStepsCard";
import { ContractClearanceReviewSection } from "@/components/contracts/ContractClearanceReviewSection";
import { ClearanceWorkflowFilesPanel } from "@/components/contracts/ClearanceWorkflowFilesPanel";
import { ContractRevisionTimeline } from "@/components/contracts/ContractRevisionTimeline";
import {
@@ -74,15 +72,14 @@ import {
import type { CustomerDocument } from "@/types/customer";
import type { Freight } from "@edr/types";
// Clearance phase — staff can still ACT (approve / query / finalize).
// Clearance phase — actionable (docs approve / query / finalize on the hub).
const CLEARANCE_ACTIVE_STATUSES = [
"AWAITING_CLEARANCE_DOCUMENTS",
"CLEARANCE_UNDER_REVIEW",
"CLEARANCE_READY_FOR_BOOKING",
];
// Clearance is done — the tab stays visible but READ-ONLY so staff/customer can
// see which documents were approved, by whom, and when.
// Clearance is done — its documents are still worth loading (read-only record).
const CLEARANCE_DONE_STATUSES = [
"ACTIVE_SHIPMENT_IN_PROGRESS",
"FULLY_EXECUTED",
@@ -91,7 +88,9 @@ const CLEARANCE_DONE_STATUSES = [
"EXPIRED",
];
// Show the Clearance Review tab in either phase (active or done).
// Contract is in (or past) its clearance phase — load the clearance view so the
// Documents tab can show customs workflow files, and surface the "Review
// clearance" deep-link to the Operations hub.
const CLEARANCE_REVIEW_STATUSES = [
...CLEARANCE_ACTIVE_STATUSES,
...CLEARANCE_DONE_STATUSES,
@@ -151,13 +150,13 @@ export default function ContractRequestDetailPage() {
}
};
const showClearanceTabQuery = Boolean(
const hasClearancePhase = Boolean(
contract && CLEARANCE_REVIEW_STATUSES.includes(contract.status),
);
const { data: clearanceView } = useQuery({
queryKey: QUERY_KEYS.CONTRACTS.clearance(id ?? ""),
queryFn: () => contractsService.getClearance(id!),
enabled: Boolean(id) && showClearanceTabQuery,
enabled: Boolean(id) && hasClearancePhase,
});
// Customer profile documents (national ID, TIN, import/business license) for
@@ -270,18 +269,10 @@ export default function ContractRequestDetailPage() {
contract.status === "APPROVED_PENDING_SIGNATURE" ||
contract.status === "REJECTED";
const showClearanceTab = CLEARANCE_REVIEW_STATUSES.includes(contract.status);
const phasedCustoms =
contract.contractKind === "ONE_TIME" && Boolean(contract.customsClearingEnabled);
const docsPhaseComplete =
clearanceView?.milestones?.some(
(m) => m.milestoneCode === "DOCUMENTS_APPROVED" && m.status === "COMPLETED",
) ?? false;
const clearanceApprovalsLocked = phasedCustoms && docsPhaseComplete;
// Once clearance is finalized the tab is informational only — no approve/query.
const clearanceReadOnly = CLEARANCE_DONE_STATUSES.includes(contract.status);
// Path A (no customs) → Operations reviews; Path B (customs) → GL reviews.
const selfClear = !contract.customsClearingEnabled;
// Clearance review + finalize now lives solely on the Operations "Clearance
// Documents" hub. The Staff-actions "Review clearance" button deep-links there
// while the contract is in a clearance-review status — no embedded tab here.
const inClearanceReview = CLEARANCE_REVIEW_STATUSES.includes(contract.status);
const files = contract.files ?? [];
const contractPdf = files.find((f) => f.code === "contract");
// Signature files (code `signature_<role>`) are baked into the contract PDF —
@@ -303,9 +294,7 @@ export default function ContractRequestDetailPage() {
? "documents"
: requestedTab === "customer"
? "customer"
: requestedTab === "clearance" && showClearanceTab
? "clearance"
: "details";
: "details";
const customerLabel = contract.isGovernment
? (contract.governmentInstitution ?? "Government")
@@ -485,39 +474,13 @@ export default function ContractRequestDetailPage() {
<Tabs.Tab value="customer" leftSection={<Users size={16} />}>
Customer
</Tabs.Tab>
{showClearanceTab && (
<Tabs.Tab
value="clearance"
leftSection={<ShieldCheck size={16} />}
>
Clearance Review
</Tabs.Tab>
)}
</Tabs.List>
</Tabs>
<Grid gap="lg">
{/* LEFT — primary content */}
<Grid.Col span={{ base: 12, lg: 8 }}>
{currentTab === "clearance" ? (
<Stack gap="lg">
<ContractClearanceReviewSection
contractId={id!}
selfClear={selfClear}
readOnly={clearanceReadOnly}
phasedCustoms={phasedCustoms}
approvalsLocked={clearanceApprovalsLocked}
onChanged={() => refetch()}
/>
{(clearanceView?.workflowFiles?.length ?? 0) > 0 ? (
<ClearanceWorkflowFilesPanel
files={clearanceView!.workflowFiles!}
onView={view}
onDownload={(f) => void handleDownloadFile({ id: f.id, name: f.name } as never)}
/>
) : null}
</Stack>
) : currentTab === "documents" ? (
{currentTab === "documents" ? (
<Stack gap="lg">
<ContractDocumentsCard
files={contractDocuments}
@@ -727,7 +690,12 @@ export default function ContractRequestDetailPage() {
contract={contract}
mutations={mutations}
onReviewClearance={
showClearanceTab ? () => setTab("clearance") : undefined
inClearanceReview
? () =>
navigate(
`/dashboard/contracts/clearance-documents/${contract.id}`,
)
: undefined
}
/>
{showApprovalCard && (