enhance contract review and editing experience

This commit is contained in:
Marshal
2026-06-30 15:16:27 +00:00
parent f51c015814
commit 708bd75d1f
31 changed files with 2044 additions and 693 deletions

View File

@@ -7,13 +7,16 @@ import {
Calendar,
CalendarClock,
FileText,
Files,
Flame,
LayoutGrid,
Package,
Receipt,
RefreshCw,
Route as RouteIcon,
ShieldCheck,
Snowflake,
Users,
} from "lucide-react";
import {
Badge,
@@ -31,6 +34,8 @@ import {
Title,
} from "@mantine/core";
import toast from "react-hot-toast";
import "@/components/overview/overview.css";
import { PageContainer } from "@/components/page";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
@@ -41,11 +46,18 @@ import { ContractWorkflowStepper } from "@/components/contracts/ContractWorkflow
import { ContractActionsToolbar } from "@/components/contracts/ContractActionsToolbar";
import { ContractApprovalStepsCard } from "@/components/contracts/ContractApprovalStepsCard";
import { ContractClearanceReviewSection } from "@/components/contracts/ContractClearanceReviewSection";
import {
ContractCustomerCard,
ContractDocumentsCard,
} from "@/components/contracts/detail/ContractDetailTabCards";
import { getContractStatusMeta } from "@/features/contracts/contract-status.config";
import { useFileViewer } from "@/hooks/useFileViewer";
import {
useContractDetail,
useContractMutations,
} from "@/hooks/contracts/useContracts";
import { downloadBookingFile } from "@/services/files.service";
import type { Freight } from "@edr/types";
// Clearance phase — staff can still ACT (approve / query / finalize).
const CLEARANCE_ACTIVE_STATUSES = [
@@ -94,7 +106,8 @@ export default function ContractRequestDetailPage() {
} = useContractDetail(id);
const mutations = useContractMutations(id ?? "");
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = searchParams.get("tab") === "clearance" ? "clearance" : "details";
const { view, viewer } = useFileViewer();
const requestedTab = searchParams.get("tab");
const setTab = (tab: string) =>
setSearchParams(
(prev) => {
@@ -106,6 +119,23 @@ export default function ContractRequestDetailPage() {
{ replace: true },
);
const handleViewFile = (file: NonNullable<Freight.IContract["files"]>[number]) =>
view({
name: file.name,
url: file.signedUrl ?? file.url,
mimeType: file.mimeType,
});
const handleDownloadFile = async (
file: NonNullable<Freight.IContract["files"]>[number],
) => {
try {
await downloadBookingFile(file.id, file.name);
} catch {
toast.error("Could not download file.");
}
};
if (isLoading) {
return (
<PageContainer>
@@ -176,9 +206,17 @@ export default function ContractRequestDetailPage() {
const clearanceReadOnly = CLEARANCE_DONE_STATUSES.includes(contract.status);
// Path A (no customs) → Operations reviews; Path B (customs) → GL reviews.
const selfClear = !contract.customsClearingEnabled;
// If the tab param points at clearance but the contract isn't in a clearance
// phase, fall back to details so we never show an empty tab.
const currentTab = activeTab === "clearance" && showClearanceTab ? "clearance" : "details";
const files = contract.files ?? [];
// Resolve the active tab from the URL, falling back to details when the
// requested tab isn't available for this contract (e.g. clearance pre-phase).
const currentTab =
requestedTab === "documents"
? "documents"
: requestedTab === "customer"
? "customer"
: requestedTab === "clearance" && showClearanceTab
? "clearance"
: "details";
const customerLabel = contract.isGovernment
? (contract.governmentInstitution ?? "Government")
@@ -264,27 +302,43 @@ export default function ContractRequestDetailPage() {
description={statusMeta.description}
/>
{showClearanceTab && (
<Tabs
value={currentTab}
onChange={(v) => setTab(v ?? "details")}
variant="pills"
color="edr-green"
classNames={{ list: "ov-tablist", tab: "ov-tab" }}
>
<Tabs.List>
<Tabs.Tab value="details" leftSection={<FileText size={16} />}>
Details
</Tabs.Tab>
<Tabs
value={currentTab}
onChange={(v) => setTab(v ?? "details")}
variant="pills"
color="edr-green"
classNames={{ list: "ov-tablist", tab: "ov-tab" }}
>
<Tabs.List>
<Tabs.Tab value="details" leftSection={<LayoutGrid size={16} />}>
Details
</Tabs.Tab>
<Tabs.Tab
value="documents"
leftSection={<Files size={16} />}
rightSection={
files.length > 0 ? (
<Badge size="xs" variant="light" color="gray" radius="sm">
{files.length}
</Badge>
) : null
}
>
Documents
</Tabs.Tab>
<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>
)}
)}
</Tabs.List>
</Tabs>
<Grid gap="lg">
{/* LEFT — primary content */}
@@ -296,6 +350,14 @@ export default function ContractRequestDetailPage() {
readOnly={clearanceReadOnly}
onChanged={() => refetch()}
/>
) : currentTab === "documents" ? (
<ContractDocumentsCard
files={files}
onView={handleViewFile}
onDownload={handleDownloadFile}
/>
) : currentTab === "customer" ? (
<ContractCustomerCard contract={contract} />
) : (
<Stack gap="lg">
<SectionCard icon={RouteIcon} title="Routes">
@@ -453,6 +515,8 @@ export default function ContractRequestDetailPage() {
</Grid.Col>
</Grid>
</Stack>
{viewer}
</PageContainer>
);
}