mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 10:45:44 +00:00
Add ContractCourtBadge component and integrate into contract pages
- Introduced ContractCourtBadge to display the responsible party for contract actions. - Updated ContractStatusBadge to include new court badge. - Enhanced ClearanceDocumentsPage with additional filters for trade direction, freight type, and ownership. - Modified ContractRequestDetailPage and ContractRequestsPage to utilize ContractCourtBadge.
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
} from "@mantine/core";
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { FileText, Inbox, RefreshCw, Search, User, X } from "lucide-react";
|
||||
@@ -16,6 +17,7 @@ import { useCallback, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
|
||||
import { ContractReferenceLink } from "@/components/bookings/ContractReferenceLink";
|
||||
import { bookingTable } from "@/components/bookings/booking-ui.styles";
|
||||
import { PageContainer, PageHeader } from "@/components/page";
|
||||
import { bookingsService } from "@/services/bookings.service";
|
||||
@@ -49,6 +51,34 @@ const BOOKING_STATUS_OPTIONS = [
|
||||
{ value: "CLEARANCE_READY", label: "Clearance ready" },
|
||||
];
|
||||
|
||||
const TRADE_DIRECTION_OPTIONS = [
|
||||
{ value: "IMPORT", label: "Import" },
|
||||
{ value: "EXPORT", label: "Export" },
|
||||
{ value: "DOMESTIC", label: "Domestic" },
|
||||
];
|
||||
|
||||
const FREIGHT_TYPE_OPTIONS = [
|
||||
{ value: "CONTAINER", label: "Container" },
|
||||
{ value: "BULK", label: "Bulk" },
|
||||
];
|
||||
|
||||
const OWNERSHIP_OPTIONS = [
|
||||
{ value: "true", label: "Government" },
|
||||
{ value: "false", label: "Private" },
|
||||
];
|
||||
|
||||
function startOfDayIso(d: Date): string {
|
||||
const x = new Date(d);
|
||||
x.setHours(0, 0, 0, 0);
|
||||
return x.toISOString();
|
||||
}
|
||||
|
||||
function endOfDayIso(d: Date): string {
|
||||
const x = new Date(d);
|
||||
x.setHours(23, 59, 59, 999);
|
||||
return x.toISOString();
|
||||
}
|
||||
|
||||
export default function ClearanceDocumentsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -56,6 +86,11 @@ export default function ClearanceDocumentsPage() {
|
||||
const [bookingStatuses, setBookingStatuses] = useState(
|
||||
BOOKING_STATUS_OPTIONS[0].value,
|
||||
);
|
||||
const [directionFilter, setDirectionFilter] = useState<string | null>(null);
|
||||
const [freightTypeFilter, setFreightTypeFilter] = useState<string | null>(null);
|
||||
const [ownershipFilter, setOwnershipFilter] = useState<string | null>(null);
|
||||
const [createdFrom, setCreatedFrom] = useState<Date | null>(null);
|
||||
const [createdTo, setCreatedTo] = useState<Date | null>(null);
|
||||
const { pagination, setPagination } = usePagination({ pageSize: PAGE_SIZE });
|
||||
|
||||
const search = debouncedQuery.trim() || undefined;
|
||||
@@ -67,7 +102,18 @@ export default function ClearanceDocumentsPage() {
|
||||
const page = pagination.pageIndex + 1;
|
||||
|
||||
const bookingsQuery = useQuery({
|
||||
queryKey: ["clearance-documents", "bookings", bookingStatuses, page, search],
|
||||
queryKey: [
|
||||
"clearance-documents",
|
||||
"bookings",
|
||||
bookingStatuses,
|
||||
directionFilter,
|
||||
freightTypeFilter,
|
||||
ownershipFilter,
|
||||
createdFrom,
|
||||
createdTo,
|
||||
page,
|
||||
search,
|
||||
],
|
||||
queryFn: () =>
|
||||
// Self-clearance instances carry bookingType=ONE_TIME whatever their
|
||||
// contract kind, so customsClearingEnabled=false + the three per-booking
|
||||
@@ -78,6 +124,13 @@ export default function ClearanceDocumentsPage() {
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
search,
|
||||
...(directionFilter ? { tradeDirection: directionFilter } : {}),
|
||||
...(freightTypeFilter ? { freightType: freightTypeFilter } : {}),
|
||||
...(ownershipFilter
|
||||
? { isGovernment: ownershipFilter as "true" | "false" }
|
||||
: {}),
|
||||
...(createdFrom ? { createdFrom: startOfDayIso(createdFrom) } : {}),
|
||||
...(createdTo ? { createdTo: endOfDayIso(createdTo) } : {}),
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
@@ -115,9 +168,18 @@ export default function ClearanceDocumentsPage() {
|
||||
{
|
||||
id: "contractRef",
|
||||
header: () => <span className={bookingTable.headerCell}>Contract</span>,
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm">{row.original.contractReference ?? "—"}</Text>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const b = row.original;
|
||||
return b.contractId && b.contractReference ? (
|
||||
<ContractReferenceLink
|
||||
contractId={b.contractId}
|
||||
contractReference={b.contractReference}
|
||||
className="block truncate text-sm text-foreground underline underline-offset-2 hover:text-muted-foreground"
|
||||
/>
|
||||
) : (
|
||||
<Text size="sm">—</Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "shipment",
|
||||
@@ -238,6 +300,73 @@ export default function ClearanceDocumentsPage() {
|
||||
{total} record{total !== 1 ? "s" : ""}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="sm" mt="sm" wrap="wrap">
|
||||
<Select
|
||||
placeholder="Direction"
|
||||
data={TRADE_DIRECTION_OPTIONS}
|
||||
value={directionFilter}
|
||||
onChange={(v) => {
|
||||
setDirectionFilter(v);
|
||||
resetPage();
|
||||
}}
|
||||
clearable
|
||||
radius="lg"
|
||||
style={{ minWidth: 130 }}
|
||||
aria-label="Filter by direction"
|
||||
/>
|
||||
<Select
|
||||
placeholder="Freight type"
|
||||
data={FREIGHT_TYPE_OPTIONS}
|
||||
value={freightTypeFilter}
|
||||
onChange={(v) => {
|
||||
setFreightTypeFilter(v);
|
||||
resetPage();
|
||||
}}
|
||||
clearable
|
||||
radius="lg"
|
||||
style={{ minWidth: 140 }}
|
||||
aria-label="Filter by freight type"
|
||||
/>
|
||||
<Select
|
||||
placeholder="Gov / Private"
|
||||
data={OWNERSHIP_OPTIONS}
|
||||
value={ownershipFilter}
|
||||
onChange={(v) => {
|
||||
setOwnershipFilter(v);
|
||||
resetPage();
|
||||
}}
|
||||
clearable
|
||||
radius="lg"
|
||||
style={{ minWidth: 140 }}
|
||||
aria-label="Filter by ownership"
|
||||
/>
|
||||
<DateInput
|
||||
placeholder="Created from"
|
||||
value={createdFrom}
|
||||
onChange={(v) => {
|
||||
setCreatedFrom(v ? new Date(v) : null);
|
||||
resetPage();
|
||||
}}
|
||||
maxDate={createdTo ?? undefined}
|
||||
clearable
|
||||
radius="lg"
|
||||
style={{ minWidth: 140 }}
|
||||
aria-label="Created from"
|
||||
/>
|
||||
<DateInput
|
||||
placeholder="Created to"
|
||||
value={createdTo}
|
||||
onChange={(v) => {
|
||||
setCreatedTo(v ? new Date(v) : null);
|
||||
resetPage();
|
||||
}}
|
||||
minDate={createdFrom ?? undefined}
|
||||
clearable
|
||||
radius="lg"
|
||||
style={{ minWidth: 140 }}
|
||||
aria-label="Created to"
|
||||
/>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
{showEmpty ? (
|
||||
|
||||
@@ -15,12 +15,14 @@ import {
|
||||
Files,
|
||||
Flame,
|
||||
History,
|
||||
Info,
|
||||
LayoutGrid,
|
||||
Milestone,
|
||||
Package,
|
||||
Receipt,
|
||||
RefreshCw,
|
||||
Route as RouteIcon,
|
||||
ShieldCheck,
|
||||
Snowflake,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
@@ -35,6 +37,7 @@ import {
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
@@ -48,7 +51,10 @@ import { PageContainer } from "@/components/page";
|
||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||
import { SectionCard } from "@/components/bookings/detail/SectionCard";
|
||||
import { detailStyles } from "@/components/bookings/detail/booking-detail.styles";
|
||||
import { ContractStatusBadge } from "@/components/contracts/ContractStatusBadge";
|
||||
import {
|
||||
ContractCourtBadge,
|
||||
ContractStatusBadge,
|
||||
} from "@/components/contracts/ContractStatusBadge";
|
||||
import { ContractWorkflowStepper } from "@/components/contracts/ContractWorkflowStepper";
|
||||
import { ContractActionsToolbar } from "@/components/contracts/ContractActionsToolbar";
|
||||
import { ContractApprovalStepsCard } from "@/components/contracts/ContractApprovalStepsCard";
|
||||
@@ -374,6 +380,7 @@ export default function ContractRequestDetailPage() {
|
||||
status={contract.status}
|
||||
isRenewal={Boolean(contract.renewalOfId)}
|
||||
/>
|
||||
<ContractCourtBadge status={contract.status} />
|
||||
<Badge variant="light" color="gray" radius="sm" tt="uppercase">
|
||||
{contract.contractKind === "GENERAL" ? "General" : "One-time"}
|
||||
</Badge>
|
||||
@@ -553,6 +560,100 @@ export default function ContractRequestDetailPage() {
|
||||
<ContractCustomerCard contract={contract} />
|
||||
) : (
|
||||
<Stack gap="lg">
|
||||
<SectionCard
|
||||
icon={Info}
|
||||
title="Contract information"
|
||||
subtitle="Full commercial and operational detail for this contract."
|
||||
>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
|
||||
<InfoRow
|
||||
label="Service type"
|
||||
value={contract.serviceType?.serviceName ?? "—"}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Payment currency"
|
||||
value={contract.paymentCurrency ?? "—"}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Customs clearing"
|
||||
value={
|
||||
contract.customsClearingEnabled
|
||||
? "Included automatically"
|
||||
: contract.customsClearingAgent
|
||||
? `Customer's agent — ${contract.customsClearingAgent}`
|
||||
: "Not included"
|
||||
}
|
||||
/>
|
||||
{contract.equipmentReturn ? (
|
||||
<InfoRow
|
||||
label="Equipment return"
|
||||
value={
|
||||
contract.equipmentReturn === "WITH_RETURN"
|
||||
? "With return"
|
||||
: "Without return"
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<InfoRow
|
||||
label="Contract type"
|
||||
value={contract.contractType ?? "Standard"}
|
||||
/>
|
||||
{contract.contractValidityDays != null ? (
|
||||
<InfoRow
|
||||
label="Validity period"
|
||||
value={`${contract.contractValidityDays} days`}
|
||||
/>
|
||||
) : null}
|
||||
{contract.estimatedShipmentDate ? (
|
||||
<InfoRow
|
||||
label="Estimated shipment date"
|
||||
value={formatDate(contract.estimatedShipmentDate)}
|
||||
/>
|
||||
) : null}
|
||||
{contract.firstMilePickupAddress ? (
|
||||
<InfoRow
|
||||
label="First-mile pickup"
|
||||
value={contract.firstMilePickupAddress}
|
||||
/>
|
||||
) : null}
|
||||
{contract.lastMileDeliveryAddress ? (
|
||||
<InfoRow
|
||||
label="Last-mile delivery"
|
||||
value={contract.lastMileDeliveryAddress}
|
||||
/>
|
||||
) : null}
|
||||
</SimpleGrid>
|
||||
{contract.financialTerms ? (
|
||||
<Box
|
||||
mt="md"
|
||||
pt="md"
|
||||
style={{ borderTop: "1px solid var(--mantine-color-gray-2)" }}
|
||||
>
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
fw={600}
|
||||
tt="uppercase"
|
||||
mb={4}
|
||||
style={{ letterSpacing: 0.3 }}
|
||||
>
|
||||
Financial terms
|
||||
</Text>
|
||||
<Text size="sm" style={{ whiteSpace: "pre-wrap" }}>
|
||||
{contract.financialTerms}
|
||||
</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={ShieldCheck}
|
||||
title="Approval & signing timeline"
|
||||
subtitle="Every dated step in this contract's approval chain, plus signatures — the same record kept in the sidebar, always visible here."
|
||||
>
|
||||
<ContractMilestonesTimeline contract={contract} />
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard icon={RouteIcon} title="Routes">
|
||||
{routes.length === 0 ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
@@ -761,6 +862,25 @@ export default function ContractRequestDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div>
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
fw={600}
|
||||
tt="uppercase"
|
||||
style={{ letterSpacing: 0.3 }}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="sm" fw={500} mt={2}>
|
||||
{value}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetaItem({
|
||||
icon: Icon,
|
||||
text,
|
||||
|
||||
@@ -34,7 +34,10 @@ import { useCallback, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { ContractApprovalProgressCell } from "@/components/contracts/ContractApprovalProgressCell";
|
||||
import { ContractStatusBadge } from "@/components/contracts/ContractStatusBadge";
|
||||
import {
|
||||
ContractCourtBadge,
|
||||
ContractStatusBadge,
|
||||
} from "@/components/contracts/ContractStatusBadge";
|
||||
import {
|
||||
ContractStatusTabs,
|
||||
type ContractStatusTabKey,
|
||||
@@ -334,6 +337,19 @@ export default function ContractRequestsPage() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "court",
|
||||
size: COLUMN_WIDTH,
|
||||
meta: COLUMN_META,
|
||||
header: () => (
|
||||
<span className={bookingTable.headerCell}>Waiting on</span>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="py-1">
|
||||
<ContractCourtBadge status={row.original.status} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "approval",
|
||||
size: COLUMN_WIDTH,
|
||||
@@ -666,7 +682,7 @@ export default function ContractRequestsPage() {
|
||||
}}
|
||||
// table-fixed makes the per-column 120px widths stick; without
|
||||
// it auto-layout re-widens columns once cells wrap.
|
||||
containerClassName="border-0 shadow-none bg-transparent [&_table]:table-fixed [&_table]:min-w-[840px]"
|
||||
containerClassName="border-0 shadow-none bg-transparent [&_table]:table-fixed [&_table]:min-w-[960px]"
|
||||
footer={DataTableFooter}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -242,23 +242,20 @@ export default function GlClearanceDetailPage() {
|
||||
|
||||
<Tabs.Panel value="workflow">
|
||||
{/* GL Ethiopia cannot file the import customs declaration until this
|
||||
desk names the officer handling the shipment in transit, so the
|
||||
ask sits above everything else on the page. Exports have no such
|
||||
gate — Djibouti's steps come after the declaration. */}
|
||||
{isImport ? (
|
||||
<Box mb="md">
|
||||
<TransitAssigneePanel
|
||||
entityId={id!}
|
||||
isBooking={data.kind === "booking"}
|
||||
transitAssignee={data.clearance.transitAssignee}
|
||||
side="DJ"
|
||||
readOnly={
|
||||
!hasPermission(user, FREIGHT_PERMS.contracts.clearanceDjActions)
|
||||
}
|
||||
onChanged={() => void refetch()}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
desk names the officer handling the shipment in transit. Exports also
|
||||
need transit assignment at the DJ stage after ET requests it. */}
|
||||
<Box mb="md">
|
||||
<TransitAssigneePanel
|
||||
entityId={id!}
|
||||
isBooking={data.kind === "booking"}
|
||||
transitAssignee={data.clearance.transitAssignee}
|
||||
side="DJ"
|
||||
readOnly={
|
||||
!hasPermission(user, FREIGHT_PERMS.contracts.clearanceDjActions)
|
||||
}
|
||||
onChanged={() => void refetch()}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, lg: 7 }}>
|
||||
|
||||
Reference in New Issue
Block a user