implement BookingsManager component for managing batch bookings with search and filter functionality

This commit is contained in:
Marshal
2026-06-28 22:23:51 +00:00
parent 506b269207
commit d6ae1ac18d
8 changed files with 791 additions and 61 deletions

View File

@@ -160,7 +160,6 @@ export function AppLayout({
const mutedColor = theme.colors["edr-muted"][6];
const textColor = theme.colors["edr-text"][6];
const accentColor = theme.colors["edr-accent"][6];
const bgColor = theme.colors["edr-bg"][6];
const primaryColor = theme.colors["edr-green"][5];
const primaryDarkColor = theme.colors["edr-green"][7];
@@ -254,12 +253,9 @@ export function AppLayout({
<AppShell.Header
withBorder={false}
style={{
backdropFilter: "blur(14px)",
WebkitBackdropFilter: "blur(14px)",
// Light translucent surface with a faint green-tinted wash on the
// right, a hairline base, and a soft drop so it floats above content.
background:
"linear-gradient(180deg, rgba(255,255,255,0.92) 0%, rgba(255,255,255,0.78) 100%)",
// Solid white surface with a hairline base and a soft drop so it
// floats above the content area.
background: "#FFFFFF",
borderBottom: `1px solid ${borderColor}`,
boxShadow: "0 1px 12px rgba(16,24,40,0.04)",
}}
@@ -512,10 +508,8 @@ export function AppLayout({
<AppShell.Navbar
withBorder={false}
style={{
// Soft light wash — a barely-there green tint at the top fading to
// white, so the rail reads as its own clean surface.
background:
"linear-gradient(180deg, #F4FAF7 0%, #FBFDFC 22%, #FFFFFF 100%)",
// Clean white rail.
background: "#FFFFFF",
borderRight: `1px solid ${borderColor}`,
display: "flex",
flexDirection: "column",
@@ -802,11 +796,7 @@ export function AppLayout({
{/* ── Main ── */}
<AppShell.Main
style={{
backgroundColor: bgColor,
backgroundImage:
"radial-gradient(58% 42% at 100% 0%, rgba(14,163,113,0.18) 0%, rgba(14,163,113,0.08) 38%, rgba(14,163,113,0.04) 72%)",
backgroundRepeat: "no-repeat",
backgroundAttachment: "fixed",
backgroundColor: "#F1F5F9",
}}
>
{children}

View File

@@ -16,17 +16,24 @@ import {
Title,
} from "@mantine/core";
import {
CheckCircle2,
ChevronLeft,
ChevronRight,
CheckCircle2,
Eye,
FileSignature,
FileStack,
Inbox,
Package,
PackagePlus,
PencilLine,
Plus,
RotateCcw,
Search,
Timer,
Upload,
Weight,
X,
type LucideIcon,
} from "lucide-react";
import { api } from "@/services/api";
@@ -53,23 +60,42 @@ function primaryRoute(contract: Freight.IContract) {
};
}
// Customs (Path B) statuses where the customer still needs to upload / manage
// clearance docs. Once finalized (CLEARANCE_READY_FOR_BOOKING) the row falls
// through to the Book action instead.
const PATH_B_CLEARANCE_STATUSES = [
"AWAITING_CLEARANCE_DOCUMENTS",
"CLEARANCE_UNDER_REVIEW",
"CLEARANCE_READY_FOR_BOOKING",
];
interface RowAction {
label: string;
to: string;
primary: boolean;
icon: LucideIcon;
}
/** The single most relevant next action for a customer's contract row. */
function getCustomerRowAction(
contract: Freight.IContract,
bookings: Freight.IBooking[],
): { label: string; to: string; primary: boolean } {
): RowAction {
const id = contract.id;
if (contract.status === "CONTRACT_READY") {
return { label: "View & sign", to: `/contracts/${id}/view`, primary: true };
return {
label: "View & sign",
to: `/contracts/${id}/view`,
primary: true,
icon: FileSignature,
};
}
if (contract.status === "CHANGES_REQUESTED") {
return { label: "Edit & resubmit", to: `/contracts/${id}`, primary: true };
return {
label: "Edit & resubmit",
to: `/contracts/${id}`,
primary: true,
icon: PencilLine,
};
}
if (
contract.customsClearingEnabled &&
@@ -79,16 +105,27 @@ function getCustomerRowAction(
label: "Upload clearance",
to: `/contracts/${id}/clearance`,
primary: true,
icon: Upload,
};
}
const booking = getContractBookingAction(contract, bookings);
if (booking.kind === "book") {
return { label: "Book shipment", to: booking.to, primary: true };
return {
label: "Book shipment",
to: booking.to,
primary: true,
icon: PackagePlus,
};
}
if (booking.kind === "rebook") {
return { label: "Re-book shipment", to: booking.to, primary: true };
return {
label: "Re-book shipment",
to: booking.to,
primary: true,
icon: RotateCcw,
};
}
return { label: "View", to: `/contracts/${id}`, primary: false };
return { label: "View", to: `/contracts/${id}`, primary: false, icon: Eye };
}
export default function ContractsList() {
@@ -338,6 +375,7 @@ export default function ContractsList() {
verticalSpacing={14}
horizontalSpacing={20}
highlightOnHover
highlightOnHoverColor="#F4FBF8"
styles={{
th: {
fontSize: 11,
@@ -348,6 +386,12 @@ export default function ContractsList() {
background: "#F8FAFC",
borderBottom: `1px solid ${BORDER}`,
whiteSpace: "nowrap",
position: "sticky",
top: 0,
zIndex: 1,
},
tr: {
transition: "background-color 120ms ease",
},
td: {
borderBottom: `1px solid ${BORDER}`,
@@ -507,16 +551,26 @@ export default function ContractsList() {
onClick={(e) => e.stopPropagation()}
/>
<Button
size="compact-sm"
size="sm"
radius="md"
h={34}
variant={action.primary ? "filled" : "light"}
color="edr-green"
leftSection={<action.icon size={15} />}
onClick={(e) => {
e.stopPropagation();
navigate(action.to);
}}
styles={{
root: { fontWeight: 600, paddingInline: 14 },
root: {
fontWeight: 600,
fontSize: 13,
paddingInline: 14,
whiteSpace: "nowrap",
boxShadow: action.primary
? "0 1px 2px rgba(14,163,113,0.25)"
: "none",
},
}}
>
{action.label}

View File

@@ -112,17 +112,24 @@ export default function NewShipmentPage() {
);
}
// Path A guard — customs contracts are GL-booked, not customer-booked.
if (contract.customsClearingEnabled) {
// Customs (Path B) contracts can only be booked once GL has finalized the
// pre-booking clearance. Before that, send the customer to the clearance step.
// (GL "create booking" was removed — the customer books once cleared.)
const clearanceFinalized =
contract.clearanceStatus === "CLEARANCE_READY_FOR_BOOKING" ||
contract.status === "CLEARANCE_READY_FOR_BOOKING" ||
contract.status === "ACTIVE_SHIPMENT_IN_PROGRESS";
if (contract.customsClearingEnabled && !clearanceFinalized) {
return (
<Box p="xl">
<Alert color="orange" icon={<AlertCircle size={18} />} radius="md">
<Text fw={700} mb="xs">
Bookings for this contract are handled by Global Logistics
Clearance not finalized yet
</Text>
<Text size="sm" mb="md">
This contract includes customs clearance. Upload your clearance
documents and Global Logistics will create the booking for you.
documents once Global Logistics finalizes the clearance you can
create your shipment booking here.
</Text>
<Button
color="edr-green"

View File

@@ -15,6 +15,16 @@ export const TERMINAL_BOOKING_STATUSES = [
/** Path A statuses where a customer (no customs) may book against the contract. */
const PATH_A_BOOKABLE = ["FULLY_EXECUTED", "CONTRACT_ACTIVE"];
/**
* Path B (customs): the customer books once GL has finalized the pre-booking
* clearance. ACTIVE_SHIPMENT_IN_PROGRESS is included so GENERAL contracts can
* re-book after a prior shipment. (GL "create booking" was removed.)
*/
const PATH_B_BOOKABLE = [
"CLEARANCE_READY_FOR_BOOKING",
"ACTIVE_SHIPMENT_IN_PROGRESS",
];
export type ContractBookingActionKind = "book" | "rebook" | "none";
export interface ContractBookingAction {
@@ -35,8 +45,10 @@ export function getContractBookingAction(
contract: Freight.IContract,
bookings: Freight.IBooking[],
): ContractBookingAction {
if (contract.customsClearingEnabled) return { kind: "none", to: "" };
if (!PATH_A_BOOKABLE.includes(contract.status)) return { kind: "none", to: "" };
const bookable = contract.customsClearingEnabled
? PATH_B_BOOKABLE.includes(contract.status)
: PATH_A_BOOKABLE.includes(contract.status);
if (!bookable) return { kind: "none", to: "" };
const to = `/contracts/${contract.id}/bookings/new`;

View File

@@ -254,7 +254,7 @@ export function ContractDocButton({
const file = contractPdfFile(contract);
if (!file) return null;
return (
<Tooltip label="Contract document">
<Tooltip label="Contract document" withArrow>
<Box
component="a"
href={fileViewUrl(file.id)}
@@ -266,12 +266,22 @@ export function ContractDocButton({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 32,
height: 32,
width: 34,
height: 34,
borderRadius: 8,
border: `1px solid ${BORDER}`,
background: "#FFFFFF",
color: GREEN_DARK,
flexShrink: 0,
transition: "border-color 120ms ease, background-color 120ms ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = GREEN;
e.currentTarget.style.backgroundColor = "#F0FAF5";
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = BORDER;
e.currentTarget.style.backgroundColor = "#FFFFFF";
}}
>
<FileText size={16} />