Implement intercity document handling and rejection notes for contracts

This commit is contained in:
Marshal
2026-07-21 10:15:18 +00:00
parent 603537a20b
commit ceb32e0a80
16 changed files with 287 additions and 23 deletions

View File

@@ -1,5 +1,5 @@
import { useMemo, useState } from "react";
import { AlertTriangle, Check, ShieldCheck, X } from "lucide-react";
import { Check, ShieldCheck, X } from "lucide-react";
import {
Stack,
Group,
@@ -51,6 +51,11 @@ export function ContractApprovalStepsCard({
const nextPending = steps.find((s) => s.status === "PENDING");
const summary = formatContractApprovalProgress(contract.status, steps);
// The card also renders read-only trails (e.g. a REJECTED contract) — only
// offer approve/reject while the backend accepts step actions.
const actionable =
contract.status === "PENDING_APPROVAL" ||
contract.status === "APPROVED_PENDING_SIGNATURE";
// Approvers review a live preview of the document; there is no PDF to
// generate first — the final approval is what produces it.
@@ -160,7 +165,7 @@ export function ContractApprovalStepsCard({
<StepRow
key={step.id}
step={step}
isNext={nextPending?.id === step.id}
isNext={actionable && nextPending?.id === step.id}
isPending={
mutations.approveStep.isPending ||
mutations.rejectStep.isPending

View File

@@ -1,6 +1,7 @@
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import {
AlertTriangle,
ArrowLeft,
ArrowRight,
Box as BoxIcon,
@@ -22,6 +23,7 @@ import {
Users,
} from "lucide-react";
import {
Alert,
Badge,
Box,
Button,
@@ -264,7 +266,8 @@ export default function ContractRequestDetailPage() {
const showApprovalCard =
contract.status === "PENDING_APPROVAL" ||
contract.status === "APPROVED" ||
contract.status === "APPROVED_PENDING_SIGNATURE";
contract.status === "APPROVED_PENDING_SIGNATURE" ||
contract.status === "REJECTED";
const showClearanceTab = CLEARANCE_REVIEW_STATUSES.includes(contract.status);
const phasedCustoms =
@@ -428,6 +431,32 @@ export default function ContractRequestDetailPage() {
description={statusMeta.description}
/>
{contract.status === "REJECTED" && contract.latestRejectionNote ? (
<Alert
color="red"
radius="md"
icon={<AlertTriangle size={18} />}
title="Rejection reason"
>
<Text size="sm" style={{ whiteSpace: "pre-wrap" }}>
{contract.latestRejectionNote}
</Text>
</Alert>
) : null}
{contract.status === "PENDING_APPROVAL" && contract.latestSendBackNote ? (
<Alert
color="orange"
radius="md"
icon={<AlertTriangle size={18} />}
title="Sent back in the approval chain"
>
<Text size="sm" style={{ whiteSpace: "pre-wrap" }}>
{contract.latestSendBackNote}
</Text>
</Alert>
) : null}
<Tabs
value={currentTab}
onChange={(v) => setTab(v ?? "details")}

View File

@@ -409,6 +409,10 @@ export default function ContractDetailPage() {
const canSign = contract.status === "CONTRACT_READY";
const customsPath = contract.customsClearingEnabled;
// Intercity (DOMESTIC) has no customs — the document gate collects the
// admin-configured intercity set, reviewed by Operations.
const isIntercity = contract.tradeDirection === "DOMESTIC";
const docNoun = isIntercity ? "intercity documents" : "clearance documents";
// Only the NON-customs (Path A) customer books himself — once the contract is
// executed after self-clearance. Customs (Path B) bookings are created by
// Global Logistics on the customer's behalf, so the customer gets no booking
@@ -586,8 +590,8 @@ export default function ContractDetailPage() {
onClick={clearanceModal.open}
>
{contract.status === "CLEARANCE_UNDER_REVIEW"
? "Manage clearance documents"
: "Upload clearance documents"}
? `Manage ${docNoun}`
: `Upload ${docNoun}`}
</Button>
)}
</Group>
@@ -852,7 +856,9 @@ export default function ContractDetailPage() {
<Text fw={700} fz={15} c={INK}>
{customsPath
? "Customs clearance shipment"
: "Customs clearance required"}
: isIntercity
? "Intercity documents required"
: "Customs clearance required"}
</Text>
</Group>
<Text fz={13} c="dimmed">
@@ -862,11 +868,17 @@ export default function ContractDetailPage() {
: contract.status === "CLEARANCE_UNDER_REVIEW"
? "Global Logistics is reviewing your clearance documents. Re-upload any queried documents to proceed."
: "Your documents are cleared. You can now create a shipment booking under this contract."
: contract.status === "AWAITING_CLEARANCE_DOCUMENTS"
? "This service does not include EDR customs clearance. Clear the cargo yourself and upload your clearance documents so the Operations team can review them before you book a shipment."
: contract.status === "CLEARANCE_UNDER_REVIEW"
? "The Operations team is reviewing your clearance documents. Re-upload any queried documents to proceed."
: "Your clearance documents are approved. You can now create a shipment booking under this contract."}
: isIntercity
? contract.status === "AWAITING_CLEARANCE_DOCUMENTS"
? "Upload the required intercity documents so the Operations team can review them before you book a shipment."
: contract.status === "CLEARANCE_UNDER_REVIEW"
? "The Operations team is reviewing your intercity documents. Re-upload any queried documents to proceed."
: "Your intercity documents are approved. You can now create a shipment booking under this contract."
: contract.status === "AWAITING_CLEARANCE_DOCUMENTS"
? "This service does not include EDR customs clearance. Clear the cargo yourself and upload your clearance documents so the Operations team can review them before you book a shipment."
: contract.status === "CLEARANCE_UNDER_REVIEW"
? "The Operations team is reviewing your clearance documents. Re-upload any queried documents to proceed."
: "Your clearance documents are approved. You can now create a shipment booking under this contract."}
</Text>
{contract.status !== "CLEARANCE_READY_FOR_BOOKING" && (
<Button
@@ -1581,7 +1593,7 @@ export default function ContractDetailPage() {
onClose={clearanceModal.close}
title={
<Text fw={700} fz={16}>
Clearance documents
{isIntercity ? "Intercity documents" : "Clearance documents"}
</Text>
}
size="xl"

View File

@@ -254,6 +254,14 @@ export function ContractStepBanner({ contract }: ContractStepBannerProps) {
</Text>
</Group>
)}
{contract.status === "REJECTED" && contract.latestRejectionNote && (
<Text
fz={12.5}
style={{ color: "#B42318", whiteSpace: "pre-wrap", width: "100%" }}
>
Reason: {contract.latestRejectionNote}
</Text>
)}
{expirySoon && (
<Group gap={6} wrap="nowrap" align="center">
<AlertTriangle size={14} color="#9A6700" />