mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 16:35:42 +00:00
implement BookingsManager component for managing batch bookings with search and filter functionality
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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`;
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user