From 456603304635c2938518ece3e10d8ae43a377416 Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Tue, 9 Jun 2026 09:27:14 +0300 Subject: [PATCH] style: ui revamp --- apps/edr-freight-web/portal/index.html | 6 + .../portal/src/components/AppLayout.tsx | 195 +- .../portal/src/components/auth/AuthLayout.tsx | 142 +- .../portal/src/pages/MyPortalPage.tsx | 319 +-- .../src/pages/bookings/BookingDetailPage.tsx | 2149 ++++++++--------- .../src/pages/bookings/EditBookingPage.tsx | 120 +- .../portal/src/pages/bookings/MyBookings.tsx | 395 +-- .../src/pages/bookings/NewBookingPage.tsx | 95 +- .../new-booking-form/StepIndicator.tsx | 16 +- .../bookings/new-booking-form/shared.tsx | 136 +- .../new-booking-form/step1-contract-type.tsx | 29 +- .../new-booking-form/step2-service-type.tsx | 144 +- .../bookings/new-booking-form/step4-route.tsx | 131 +- .../new-booking-form/step5-cargo-details.tsx | 260 +- .../new-booking-form/step8-review.tsx | 353 ++- .../portal/src/theme/mantine.ts | 6 +- 16 files changed, 2188 insertions(+), 2308 deletions(-) diff --git a/apps/edr-freight-web/portal/index.html b/apps/edr-freight-web/portal/index.html index 2233dc861..61b3dcff7 100644 --- a/apps/edr-freight-web/portal/index.html +++ b/apps/edr-freight-web/portal/index.html @@ -5,6 +5,12 @@ EDR Freight Portal + + + diff --git a/apps/edr-freight-web/portal/src/components/AppLayout.tsx b/apps/edr-freight-web/portal/src/components/AppLayout.tsx index fcc26ea4a..3484ba1f5 100644 --- a/apps/edr-freight-web/portal/src/components/AppLayout.tsx +++ b/apps/edr-freight-web/portal/src/components/AppLayout.tsx @@ -19,6 +19,7 @@ import { useDisclosure } from "@mantine/hooks"; import { Bell, ChevronDown, + ChevronRight, Languages, LogOut, Moon, @@ -93,14 +94,24 @@ function getActivePage( return null; } -// Shared NavLink styling — green tint only when active, quiet neutral otherwise. -const navLinkStyles = { - root: { - borderRadius: "var(--mantine-radius-md)", - fontWeight: 500, - }, - label: { fontSize: "var(--mantine-font-size-sm)" }, -} as const; +// NavLink classNames for the dark sidebar — Tailwind utilities (with v4 `!` +// important suffix) override Mantine's default active styling. +const navClassNames = (active: boolean) => { + const base = + "rounded-[10px] font-medium transition-all duration-150 active:scale-[0.98]"; + if (active) { + return { + root: `${base} bg-gradient-to-br! from-emerald-600! to-emerald-500! text-white! shadow-[0_2px_8px_-4px_rgba(16,185,129,0.45)]`, + label: "text-white!", + section: "text-white!", + }; + } + return { + root: `${base} text-white/60! hover:bg-white/[0.07]! hover:text-white!`, + label: "text-inherit!", + section: "text-inherit! opacity-90", + }; +}; export function AppLayout({ title = "EDR Freight", @@ -139,7 +150,7 @@ export function AppLayout({ {/* ── Header ──────────────────────────────────────────────────────────── */} @@ -159,9 +170,24 @@ export function AppLayout({ hiddenFrom="sm" size="sm" /> - - {activePage ? activePage.label : title} - + + + {title} + + + + + + {activePage ? activePage.label : title} + + {/* Right: utility actions + user menu */} @@ -170,16 +196,20 @@ export function AppLayout({ variant="subtle" color="gray" size="lg" + radius="md" + className="transition-transform hover:-translate-y-px" aria-label="Change language" > - + @@ -191,6 +221,8 @@ export function AppLayout({ variant="subtle" color="gray" size="lg" + radius="md" + className="transition-transform hover:-translate-y-px" onClick={toggleTheme} aria-label="Toggle theme" > @@ -198,38 +230,42 @@ export function AppLayout({ )} + + - - + + {initials} - - {userName} - - + + + {userName} + + + Customer + + + @@ -267,49 +303,29 @@ export function AppLayout({ {/* ── Sidebar ─────────────────────────────────────────────────────────── */} {/* Brand */} - + - + - - {title} - + + + {title} + + + Logistics Portal + + {/* Nav links */} - + {sidebarItems.map((item, i) => { const active = isItemActive(item); const hasChildren = !!item.children?.length; @@ -324,13 +340,11 @@ export function AppLayout({ {item.section} @@ -344,23 +358,18 @@ export function AppLayout({ label={item.label} leftSection={item.icon} active={active || childActive} - color="edr-green" - variant="filled" defaultOpened={childActive} - styles={navLinkStyles} + classNames={navClassNames(active || childActive)} > {item.children!.map((child) => { - const cActive = - activePath === child.href.toLowerCase(); + const cActive = activePath === child.href.toLowerCase(); return ( navigate(child.href)} - styles={navLinkStyles} + classNames={navClassNames(cActive)} /> ); })} @@ -376,10 +385,8 @@ export function AppLayout({ label={item.label} leftSection={item.icon} active={active} - color="edr-green" - variant="filled" onClick={() => navigate(item.href)} - styles={navLinkStyles} + classNames={navClassNames(active)} /> ); @@ -388,26 +395,24 @@ export function AppLayout({ {/* Bottom user */} - + - - + + {initials} - - + + {userName} {userEmail && ( - + {userEmail} )} @@ -417,7 +422,7 @@ export function AppLayout({ {/* ── Main ────────────────────────────────────────────────────────────── */} - + {children} diff --git a/apps/edr-freight-web/portal/src/components/auth/AuthLayout.tsx b/apps/edr-freight-web/portal/src/components/auth/AuthLayout.tsx index 9d023a3ec..f66df0afa 100644 --- a/apps/edr-freight-web/portal/src/components/auth/AuthLayout.tsx +++ b/apps/edr-freight-web/portal/src/components/auth/AuthLayout.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from "react"; +import { Box, Group, Stack, Text, ThemeIcon, Title } from "@mantine/core"; import { ShieldCheck, Train } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -27,67 +28,102 @@ export default function AuthLayout({ left, }: AuthLayoutProps) { return ( -
-
-
-
-
-
-
- -
-
-

EDR Freight

-

- Railway Logistics Platform -

-
-
-
-
- {left.badge} -
-

- {left.title} -

-

- {left.description} -

-
-
+ + + {/* ── Left: branded panel ─────────────────────────────────────── */} + + {/* rail-line motif */} + + {/* corner glow */} + + + {/* Brand */} + + + + + + + EDR Freight + + + Railway Logistics Platform + + + + + {/* Headline + features */} + + + {left.badge} + + + {left.title} + + + {left.description} + + + {left.features.map((item) => ( -
-
+ + -
- {item} -
+
+ + {item} + + ))} -
-
-
-
+ + + {/* spacer keeps brand pinned top / content centered */} + + + + {/* ── Right: form area ────────────────────────────────────────── */} + -
-
-
+ + {/* Mobile brand */} + + -
-
-

EDR Freight

-

+ + + + EDR Freight + + Railway Logistics Platform -

-
-
-
{children}
-
-
-
-
+ + + + {children} + + + + ); } diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx index 1c97a13f9..81ab28011 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx @@ -4,6 +4,7 @@ import { format } from "date-fns"; import { useQuery } from "@tanstack/react-query"; import { Anchor, + Avatar, Badge, Box, Button, @@ -30,6 +31,7 @@ import { Plus, Receipt, Train, + TrendingUp, UploadCloud, } from "lucide-react"; @@ -71,6 +73,10 @@ const INVOICE_STATUS_META: Record = { Cancelled: { color: "gray" }, }; +// Card hover-lift, shared via Tailwind utilities. +const LIFT = + "transition-all duration-200 hover:-translate-y-[3px] hover:shadow-[0_16px_34px_-16px_rgba(16,24,40,0.22)] hover:border-emerald-300!"; + export default function MyPortalPage() { const { user, customer } = useAuth(); const myShipments = useMemo(() => getMyShipments(), []); @@ -109,39 +115,30 @@ export default function MyPortalPage() { const paidPct = Math.round((completedInvoices / invoiceTotal) * 100); const displayName = user?.name?.en ?? user?.username ?? user?.email ?? "—"; + const initials = displayName + .split(" ") + .filter(Boolean) + .slice(0, 2) + .map((n) => n[0]?.toUpperCase()) + .join(""); const documentsComplete = !!(customer as any)?.documentsComplete; const hasOutstanding = outstandingInvoices.length > 0; + const today = format(new Date(), "EEEE, MMMM d"); return ( - + {/* ── Document setup notice ───────────────────────────────────── */} {!documentsComplete && !dismissed && ( - - + + - + Finish setting up your account @@ -157,12 +154,7 @@ export default function MyPortalPage() { to="/settings?tab=documents" size="sm" radius="xl" - style={{ - background: "#f59e0b", - color: "white", - fontWeight: 600, - flexShrink: 0, - }} + className="flex-shrink-0 bg-gradient-to-br from-amber-500 to-amber-600! font-semibold text-white!" rightSection={} > Upload docs @@ -170,56 +162,58 @@ export default function MyPortalPage() { )} - {/* ── Welcome (branded band) ──────────────────────────────────── */} - + {/* ── Welcome (branded hero) ──────────────────────────────────── */} + {/* faint rail-line motif */} + - - - Welcome back - - - {displayName} - - + + + + {initials} + + + + + {today} + + + Welcome back, {displayName.split(" ")[0]} + + + Here's what's moving across your account today. + + + @@ -231,13 +225,13 @@ export default function MyPortalPage() { } + icon={} color="blue" /> } + icon={} color="teal" /> } + icon={} color={hasOutstanding ? "red" : "edr-green"} /> } + icon={} color="edr-green" ring={{ value: paidPct, color: "edr-green" }} /> @@ -262,20 +256,31 @@ export default function MyPortalPage() { {/* Recent Bookings (left, wider) */} - + - - Recent Bookings - - Your latest freight requests - - + + + + + + Recent Bookings + + Your latest freight requests + + +
- - ) : ( -

- Pricing will be calculated after submission. -

- )} - + + + + + + + + + Draft Booking Request + + + {booking.reference} + + + + · + + Created {format(new Date(booking.createdAt), "MMM d, yyyy")} + + + + + + + + + - - - - - Required Documents - - - Provide the necessary documents for this booking. Some information - is pre-filled from your company profile. - - - - {docError && ( -
- -

{docError}

-
- )} -
-

- - Company Information (from profile) -

-
- - - - -
-

- To update your company information, go to{" "} - + + + {/* Step 2 */} + 0 + ? "border-amber-200 bg-amber-50/30" + : "border-gray-200 bg-white" + }`} + > + + 0 ? "bg-amber-500" : "bg-gray-300" + }`} > - Settings - - . -

-
+ {allDocsUploaded ? : 2} +
+ 0 ? "orange.7" : "dimmed"} + > + Step 2 + + + Upload Documents + + {allDocsUploaded + ? "All 4 documents uploaded." + : `${uploadedCount} of ${REQUIRED_DOC_FIELDS.length} documents uploaded.`} + + {!allDocsUploaded && ( + + )} + - + {/* Step 3 */} + + + + 3 + + Step 3 + + Submit Request + + Send your booking to EDR staff for review and approval. + + + + + -
-

- - Upload Booking Documents -

-
+ {/* ── Main grid ────────────────────────────────────────────────── */} + + {/* Left: Documents */} + + + + + + + Required Documents + + + All 4 documents are required before you can submit. + + + {allDocsUploaded ? ( + }> + All uploaded + + ) : ( + + {uploadedCount}/{REQUIRED_DOC_FIELDS.length} uploaded + + )} + + + {docError && ( + } mb="md"> + {docError} + + )} + + {/* Company info */} + + + + + Company Info (pre-filled from profile) + + + + + + + + + + Update in{" "} + navigate("/settings")}> + Settings + + + + + + + {/* Document slots */} + {REQUIRED_DOC_FIELDS.map((doc) => { const isUploaded = uploadedCodes.has(doc.key); + const selectedFile = selectedFiles[doc.key]; return ( -
- -
- {isUploaded ? ( - - - Uploaded - - ) : ( - <> + + + + {isUploaded ? : } + + + {doc.label} + {isUploaded && ( + Uploaded ✓ + )} + {selectedFile && !isUploaded && ( + {selectedFile.name} + )} + {!isUploaded && !selectedFile && ( + Required · Not yet uploaded + )} + + + {!isUploaded && ( + { - fileInputRefs.current[doc.key] = el; - }} + ref={(el) => { fileInputRefs.current[doc.key] = el; }} type="file" accept=".pdf,.jpg,.jpeg,.png" className="hidden" - onChange={(e) => { - handleFileSelect( - doc.key, - e.target.files?.[0] ?? null, - ); - }} + onChange={(e) => handleFileSelect(doc.key, e.target.files?.[0] ?? null)} /> - - {selectedFiles[doc.key] && ( - + + )} - + )} -
-
+ + ); })} -
+ -
- - {uploadMutation.isSuccess && ( -

- - Documents uploaded successfully -

- )} -
-
- - - - - - - - Cancel Booking - - - If you no longer need this booking, you can cancel it. - - - -

- Cancelling will terminate this booking request and cannot be - undone. -

- - - - - - - Cancel Booking - - Are you sure you want to cancel this booking? This action - cannot be undone. - - -
- - setCancelReason(e.target.value)} - autoFocus - /> -
- - - - + {anyFileSelected && ( + - -
-
-
+ {uploadMutation.isSuccess && ( + + + Documents uploaded successfully + + )} + + )} +
+ + + {/* Right: Pricing + Booking summary */} + + + {/* Pricing */} + + + + Pricing Estimate + + + Estimated cost based on your current booking details. + + {pricing ? ( + + ) : ( + + + Pricing will be calculated automatically. + + + )} + + + {/* Booking summary */} + + + + Booking Summary + + + + + Route + + + + {booking.originYard?.label ?? booking.originYard?.code ?? "—"} + + + + {booking.destinationYard?.label ?? booking.destinationYard?.code ?? "—"} + + + + + + + + + + + + + + + + {/* ── Cancel zone ──────────────────────────────────────────────── */} + + + + Danger Zone + + + Cancelling this booking is permanent and cannot be undone. + + - - + + {/* Cancel modal */} + setCancelDialogOpen(false)} + title={Cancel Booking} + radius="lg" + centered + > + + + Are you sure you want to cancel {booking.reference}? This action + cannot be undone. + + setCancelReason(e.currentTarget.value)} + radius="md" + data-autofocus + /> + + + + + + + + ); } +// ─── Readonly View ──────────────────────────────────────────────────────────── + function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { const navigate = useNavigate(); - const queryClient = useQueryClient(); const payMutation = useMutation({ mutationFn: () => api.bookings.pay.call({ id: booking.id }), onSuccess: (data) => { - if (data.redirectUrl) { - window.location.href = data.redirectUrl; - } + if (data.redirectUrl) window.location.href = data.redirectUrl; }, }); const normalizedStatus = booking.status as keyof typeof STATUS_MAP; const statusConfig = STATUS_MAP[normalizedStatus] || STATUS_MAP.DRAFT; const currentStageIndex = statusConfig.stage; - const pricing = booking.pricingBreakdown; - const uploadedCodes = useMemo( - () => new Set(booking.files?.map((f) => f.code) ?? []), - [booking.files], - ); - return ( -
-
+ + - - -
-
- -
-
-
-

- {booking.reference} -

+ {/* ── Hero ─────────────────────────────────────────────────────── */} + + + + + + + + + + {booking.freightType === "CONTAINER" ? "Container" : "Bulk"} ·{" "} + {booking.tradeDirection ?? "Booking"} + + + {booking.reference} + + -
-
- - + · + + {format( new Date(booking.scheduledDate ?? booking.createdAt), - "MMM d, yyyy HH:mm", + "MMM d, yyyy", )} - -
-
- {normalizedStatus === "FULLY_EXECUTED" && booking.paymentStatus !== "PAID" && ( - - )} -
-
+ + +
+ + {normalizedStatus === "FULLY_EXECUTED" && booking.paymentStatus !== "PAID" && ( + + )} + - {renderContractCard(booking, navigate, payMutation)} + {/* ── Contract card ────────────────────────────────────────────── */} + {renderContractCard(booking, navigate)} - {pricing && ( - - - - - Pricing Breakdown - - - -
- - - - - - - - - {pricing.lineItems.map((item, i) => ( - - - - - ))} - - - - - -
DescriptionAmount
{item.description} - {item.amount.toLocaleString()} {item.currency} -
Total Estimated Cost - {pricing.totalAmount.toLocaleString()} {pricing.currency} -
-
-
-
- )} + {/* ── Progress & status ────────────────────────────────────────── */} + + + + Booking Progress + - {booking.files && booking.files.length > 0 && ( - - - - - Uploaded Documents ({booking.files.length}) - - - - - - - )} - - - - - - Booking Status Lifecycle - - - Track the journey from request to completion - - - -
-
-
= 0 - ? `${(currentStageIndex / (PROGRESS_STAGES.length - 1)) * 100}%` - : "0%", - }} - /> -
- - {PROGRESS_STAGES.map((stage, idx) => { - const isCompleted = idx < currentStageIndex; - const isActive = idx === currentStageIndex; - - return ( -
: } + + -
- {isCompleted ? ( - - ) : ( - - )} -
- - {stage.label} - -
- ); - })} -
+ {stage.label} + + + ); + })} + -
-
- {normalizedStatus === "CANCELLED" ? ( - + {/* Current status banner */} + + + + {normalizedStatus === "CANCELLED" || normalizedStatus === "REJECTED" ? ( + ) : ( - + )} -
-
-

+ + {statusConfig.title} -

-

+ + {statusConfig.description} -

-
+ + {normalizedStatus !== "CANCELLED" && - normalizedStatus !== "DELIVERED" && ( -
-
-

- Est. Waiting -

-

- 1-2 Working Days -

-
- -
+ normalizedStatus !== "DELIVERED" && + normalizedStatus !== "COMPLETED" && ( + + Est. Waiting + 1–2 Working Days + )} -
- + + -
-
- - - - - Route & Service - - - -
- } - /> -
-
- - -
- + {/* ── Route + Cargo (2 col) ─────────────────────────────────────── */} + + + + + + Route & Service + + + {/* Origin → Destination */} + + + + + Origin + + + + + + {booking.originYard?.label ?? booking.originYard?.code ?? "—"} + + + + + + + + Rail -
- } - /> -
+ + + + Destination + + + + + + {booking.destinationYard?.label ?? booking.destinationYard?.code ?? "—"} + + + + -
- } - label="Service" - value={ - booking.serviceType === "RAIL_AND_FORWARDING" - ? "Rail & Forwarding" - : "Rail Only" - } - /> - } - label="Return" - value={ - booking.equipmentReturn === "WITH_RETURN" - ? "With Return" - : "Without Return" - } - /> - } - label="Trade" - value={ - booking.tradeDirection === "IMPORT" ? "Import" : "Export" - } - /> -
-
+ + } label="Service" value={booking.serviceType === "RAIL_AND_FORWARDING" ? "Rail & Forwarding" : "Rail Only"} /> + } label="Return" value={booking.equipmentReturn === "WITH_RETURN" ? "With Return" : "Without Return"} /> + } label="Trade" value={booking.tradeDirection === "IMPORT" ? "Import" : "Export"} /> +
+ - - - - - Mile Services - - - -
-

+ + + + + Cargo Specifications + + + + } label="Freight Type" value={booking.freightType === "BULK" ? "Bulk" : "Break Bulk"} /> + } label="Total Weight" value={`${booking.cargoTotalWeightVgm} t`} /> + } label="Currency" value={booking.paymentCurrency} /> + } label="Hazardous" value={booking.isHazardous ? "Yes" : "No"} /> + + + {booking.containers && booking.containers.length > 0 && ( + <> + + + Load Details + + + + + + Type + Qty + VGM + + + + {booking.containers.map((c, i) => ( + + {c.type} + {c.qty} + {c.vgm}t + + ))} + +
+
+ + )} +
+
+ + + {/* ── Mile services + Contract info ─────────────────────────────── */} + + + + + + Mile Services + + + + First Mile -

- -
-
-

+ + + {booking.firstMileEnabled && booking.firstMilePickupAddress + ? booking.firstMilePickupAddress + : "Not requested"} + + + + Last Mile -

-

+ + {booking.lastMileEnabled && booking.lastMileDeliveryAddress ? booking.lastMileDeliveryAddress : "Not requested"} -

-
-
+ + +
+ - - - - - Cargo Specifications - - - -
- } - label="Freight Type" - value={ - booking.freightType === "BULK" ? "Bulk" : "Break Bulk" - } - /> - } - label="Weight (VGM)" - value={`${booking.cargoTotalWeightVgm} Tons`} - /> - } - label="Currency" - value={booking.paymentCurrency} - /> -
- - {booking.containers && booking.containers.length > 0 && ( - <> - -
-

- Load Details -

-
- - - - - - - - - - {booking.containers.map((c, i) => ( - - - - - - ))} - -
Type - Quantity - - VGM (Tons) -
- {c.type} - - {c.qty} Units - - {c.vgm}t -
-
-
- - )} -
-
-
- -
- - - - - Contract Info - - - - - - -
- + + + + + Contract Info + + + + + + + Hazardous: {booking.isHazardous ? "Yes" : "No"} - + Refrigerated: {booking.isRefrigerated ? "Yes" : "No"} -
-
+ +
+ + - - - Additional Info - - - {booking.freightSubtype && ( -
-

- Cargo Description -

-

- "{booking.freightSubtype}" -

-
- )} - {booking.financialTerms && ( - <> - -
-

- Financial Terms -

-
-

- - {booking.financialTerms} -

-
-
- - )} - {!booking.freightSubtype && !booking.financialTerms && ( -

- No additional information provided. -

- )} -
-
-
-
-
-
+ {/* ── Pricing + Documents ───────────────────────────────────────── */} + {(pricing || (booking.files && booking.files.length > 0)) && ( + + {pricing && ( + + + + + Pricing Breakdown + + + + + )} + {booking.files && booking.files.length > 0 && ( + + + + + Uploaded Documents ({booking.files.length}) + + + {booking.files.map((file) => ( + + + + + + {file.name} + {file.code.replace(/_/g, " ")} + + + ))} + + + + )} + + )} + + {/* ── Additional info ───────────────────────────────────────────── */} + {(booking.freightSubtype || booking.financialTerms) && ( + + Additional Information + + {booking.freightSubtype && ( + + + Cargo Description + + "{booking.freightSubtype}" + + )} + {booking.financialTerms && ( + <> + {booking.freightSubtype && } + + + Financial Terms + + + + + {booking.financialTerms} + + + + + )} + + + )} + + ); } +// ─── Contract card ──────────────────────────────────────────────────────────── + function renderContractCard( booking: Freight.IBooking, navigate: ReturnType, - payMutation: { mutate: () => void; isPending: boolean }, ) { const s = booking.status; if ( @@ -1283,145 +1178,177 @@ function renderContractCard( return null; } - const config: Record< - string, - { title: string; description: string; buttonLabel?: string } - > = { + const config: Record = { APPROVED_PENDING_SIGNATURE: { title: "Contract being prepared", - description: - "Your booking has been approved. The contract is being generated and will be available shortly.", + description: "Your booking has been approved. The contract will be available shortly.", }, CONTRACT_READY: { - title: "Contract ready for signature", - description: - "Review the agreement and apply your digital signature.", - buttonLabel: "View & sign contract", + title: "Action required — sign your contract", + description: "Your contract is ready. Review the agreement and apply your digital signature to proceed.", + buttonLabel: "View & Sign Contract", + urgent: true, }, SIGNED_CUSTOMER: { title: "You have signed the contract", - description: - "Your signature has been submitted. Awaiting staff signature to finalize.", - buttonLabel: "View contract", + description: "Your signature has been submitted. Awaiting the final staff signature.", + buttonLabel: "View Contract", }, FULLY_EXECUTED: { title: "Contract fully executed", - description: - "The contract has been fully signed and executed by all parties.", - buttonLabel: "View contract", + description: "The contract has been signed by all parties. You can now proceed to payment.", + buttonLabel: "View Contract", }, }; const c = config[s]; + const isUrgent = c.urgent; return ( - -
-

{c.title}

-

{c.description}

-
+ + + + + + + + {c.title} + + + {c.description} + + + {c.buttonLabel && ( - + )} -
+
); } -function RouteEndpoint({ - label, - station, - icon, +// ─── Shared sub-components ──────────────────────────────────────────────────── + +function PricingTable({ + pricing, }: { - label: string; - station: string; - icon: React.ReactNode; + pricing: { + lineItems: { description: string; amount: number; currency: string }[]; + totalAmount: number; + currency: string; + }; }) { return ( -
-
- {icon &&
{icon}
} -
-
-

- {label} -

-

{station}

-
-
+ + + + + Description + Amount + + + + {pricing.lineItems.map((item, i) => ( + + {item.description} + + {item.amount.toLocaleString()} {item.currency} + + + ))} + + Total Estimated Cost + + {pricing.totalAmount.toLocaleString()} {pricing.currency} + + + +
+
); } -function InfoItem({ - icon, +function MiniInfo({ label, value, + icon, }: { - icon?: React.ReactNode; label: string; value?: string | number | null; + icon?: React.ReactNode; }) { return ( -
- {icon && ( -
- {icon} -
- )} -
-

+ + {icon ? ( + + {icon} + + {label} + + + ) : ( + {label} -

-

{value ?? "—"}

-
-
+ + )} + {value ?? "—"} + ); } function StatusBadge({ status }: { status: string }) { - const statusColors: Record = { - DRAFT: "bg-muted text-muted-foreground border-border", - CHANGES_REQUESTED: "bg-amber-50 text-amber-700 border-amber-200", - SUBMITTED: "bg-primary/10 text-primary border-primary/20", - PENDING_APPROVAL: "bg-primary/10 text-primary border-primary/20", - APPROVED_PENDING_SIGNATURE: "bg-primary/10 text-primary border-primary/20", - APPROVED: "bg-primary/10 text-primary border-primary/20", - CONTRACT_READY: "bg-primary/10 text-primary border-primary/20", - SIGNED_CUSTOMER: "bg-primary/10 text-primary border-primary/20", - FULLY_EXECUTED: "bg-primary/10 text-primary border-primary/20", - PNR_GENERATED: "bg-primary/10 text-primary border-primary/20", - PAYMENT_VERIFICATION_IN_PROGRESS: "bg-primary/10 text-primary border-primary/20", - PAID: "bg-primary/10 text-primary border-primary/20", - CONFIRMED: "bg-primary/10 text-primary border-primary/20", - IN_TRANSIT: "bg-primary/10 text-primary border-primary/20", - PENDING_CONSOLIDATION: "bg-primary/10 text-primary border-primary/20", - CONSOLIDATED: "bg-primary/10 text-primary border-primary/20", - COMPLETED: "bg-muted text-foreground border-border", - DELIVERED: "bg-muted text-foreground border-border", - REJECTED: "bg-destructive/10 text-destructive border-destructive/20", - CANCELLED: "bg-destructive/10 text-destructive border-destructive/20", + const colorMap: Record = { + DRAFT: "gray", + CHANGES_REQUESTED: "yellow", + SUBMITTED: "edr-green", + PENDING_APPROVAL: "edr-green", + APPROVED_PENDING_SIGNATURE: "edr-green", + APPROVED: "edr-green", + CONTRACT_READY: "edr-green", + SIGNED_CUSTOMER: "edr-green", + FULLY_EXECUTED: "edr-green", + PNR_GENERATED: "edr-green", + PAYMENT_VERIFICATION_IN_PROGRESS: "edr-green", + PAID: "edr-green", + CONFIRMED: "edr-green", + IN_TRANSIT: "edr-green", + PENDING_CONSOLIDATION: "edr-green", + CONSOLIDATED: "edr-green", + COMPLETED: "gray", + DELIVERED: "gray", + REJECTED: "red", + CANCELLED: "red", }; return ( {status.replace(/_/g, " ")} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx index b72d18ee1..543817f92 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx @@ -41,11 +41,7 @@ import { type BookingFormValues, type RouteDirection, } from "./new-booking-form/schema"; -import { - SelectField, - SelectItem, - AlertBox, -} from "./new-booking-form/shared"; +import { SelectField, AlertBox } from "./new-booking-form/shared"; function yardNameFromBooking(yard: { label?: string; code?: string; name?: string } | undefined | null): string { return yard?.label ?? yard?.name ?? yard?.code ?? ""; @@ -399,10 +395,11 @@ export default function EditBookingPage() { error={fieldState.error} label="Contract Type *" placeholder="Select contract type..." - > - New Contract - Contract Renewal - + data={[ + { value: "new", label: "New Contract" }, + { value: "renewal", label: "Contract Renewal" }, + ]} + /> )} /> @@ -431,10 +428,11 @@ export default function EditBookingPage() { error={fieldState.error} label="Service Type *" placeholder="Select service type..." - > - Rail Transport Only - Logistics (Rail + Forwarding) - + data={[ + { value: "rail", label: "Rail Transport Only" }, + { value: "rail_forwarding", label: "Logistics (Rail + Forwarding)" }, + ]} + /> )} /> @@ -447,10 +445,11 @@ export default function EditBookingPage() { error={fieldState.error} label="Equipment Return" placeholder="Select..." - > - With Return - Without Return - + data={[ + { value: "with_return", label: "With Return" }, + { value: "without_return", label: "Without Return" }, + ]} + /> )} />
@@ -602,19 +601,8 @@ export default function EditBookingPage() { label="Origin Yard *" placeholder="Select origin..." disabled={yardOptions.length === 0} - > - {yardOptions.length === 0 ? ( - No yards available - ) : ( - yardOptions - .filter((y) => y.value !== destinationYard) - .map((y) => ( - - {y.label} - - )) - )} - + data={yardOptions.filter((y) => y.value !== destinationYard)} + /> )} /> @@ -628,19 +616,8 @@ export default function EditBookingPage() { label="Destination Yard *" placeholder="Select destination..." disabled={yardOptions.length === 0} - > - {yardOptions.length === 0 ? ( - No yards available - ) : ( - yardOptions - .filter((y) => y.value !== originYard) - .map((y) => ( - - {y.label} - - )) - )} - + data={yardOptions.filter((y) => y.value !== originYard)} + /> )} /> @@ -664,13 +641,8 @@ export default function EditBookingPage() { error={fieldState.error} label="Shipping Line" placeholder="Select shipping line..." - > - {shippingLineOptions.map((sl) => ( - - {sl.label} - - ))} - + data={shippingLineOptions} + /> )} /> )} @@ -736,10 +708,11 @@ export default function EditBookingPage() { error={fieldState.error} label="Cargo Type *" placeholder="Select cargo type..." - > - Containerized - General Cargo - + data={[ + { value: "container", label: "Containerized" }, + { value: "bulk", label: "General Cargo" }, + ]} + /> )} /> @@ -782,13 +755,11 @@ export default function EditBookingPage() { error={fieldState.error} label="Freight Type *" placeholder="Select freight type..." - > - {freightTypeGroups.map((group) => ( - - {group.name} - - ))} - + data={freightTypeGroups.map((g) => ({ + value: g.code.toLowerCase(), + label: g.name, + }))} + /> )} /> @@ -802,13 +773,8 @@ export default function EditBookingPage() { error={fieldState.error} label="Commodity *" placeholder="Select commodity..." - > - {commodityOptions.map((option) => ( - - {option} - - ))} - + data={commodityOptions} + /> )} /> )} @@ -903,10 +869,11 @@ export default function EditBookingPage() { error={fieldState.error} label="Size *" placeholder="Size..." - > - 20ft (TEU) - 40ft (FEU) - + data={[ + { value: "20ft", label: "20ft (TEU)" }, + { value: "40ft", label: "40ft (FEU)" }, + ]} + /> )} /> @@ -919,13 +886,8 @@ export default function EditBookingPage() { error={fieldState.error} label="Type *" placeholder="Type..." - > - {containerTypeOptions.map((option) => ( - - {option} - - ))} - + data={containerTypeOptions} + /> )} /> diff --git a/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx b/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx index ac0b3661f..3c863eb3d 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx @@ -1,6 +1,21 @@ import { useMemo, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; +import { + ActionIcon, + Badge, + Box, + Button, + Card, + Group, + Menu, + SimpleGrid, + Stack, + Text, + TextInput, + ThemeIcon, + Title, +} from "@mantine/core"; import { ArrowRight, Clock, @@ -20,17 +35,6 @@ import { DataTableFooter, type ColumnDef, usePagination, - Button, - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, - Input, - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, } from "@edr/ui-common"; export default function MyBookings() { @@ -80,15 +84,19 @@ export default function MyBookings() { cell: ({ row }) => { const booking = row.original; return ( -
-
+ + -
-
-

{booking.reference}

-

{booking.scheduledDate ?? booking.createdAt}

-
-
+ + + + {booking.reference} + + + {booking.scheduledDate ?? booking.createdAt} + + + ); }, }, @@ -96,11 +104,15 @@ export default function MyBookings() { id: "route", header: "Route", cell: ({ row }) => ( -
- {row.original.originYard?.label ?? row.original.originYard?.code ?? "—"} - - {row.original.destinationYard?.label ?? row.original.destinationYard?.code ?? "—"} -
+ + + {row.original.originYard?.label ?? row.original.originYard?.code ?? "—"} + + + + {row.original.destinationYard?.label ?? row.original.destinationYard?.code ?? "—"} + + ), }, { @@ -111,12 +123,15 @@ export default function MyBookings() { const containerCount = b.containers?.reduce((sum, c) => sum + c.qty, 0) ?? 0; const containerType = b.containers?.[0]?.type ?? null; return ( -
-

{b.freightType === "BULK" ? "Bulk" : "Break Bulk"}

-

- {containerType && containerCount > 0 ? `${containerCount} × ${containerType} · ` : ""}{b.cargoTotalWeightVgm}t -

-
+ + + {b.freightType === "BULK" ? "Bulk" : "Break Bulk"} + + + {containerType && containerCount > 0 ? `${containerCount} × ${containerType} · ` : ""} + {b.cargoTotalWeightVgm}t + + ); }, }, @@ -124,9 +139,9 @@ export default function MyBookings() { id: "transportMode", header: "Transport", cell: ({ row }) => ( - + {row.original.serviceType === "RAIL_AND_FORWARDING" ? "Rail & Forwarding" : "Rail"} - + ), }, { @@ -140,26 +155,23 @@ export default function MyBookings() { cell: ({ row }) => { const booking = row.original; return ( -
e.stopPropagation()} - > - - - - - - e.stopPropagation()}> + + + + + + + + } onClick={() => navigate(`/bookings/${booking.id}`)} > - View - - - -
+ + +
+
); }, }, @@ -168,148 +180,191 @@ export default function MyBookings() { const dataTableStatus = isLoading ? "loading" : isError ? "error" : "success"; return ( -
-
- -
-

- My Bookings -

-

- View and manage your freight booking requests. -

-
+ + + {/* ── Header band ─────────────────────────────────────────── */} + + + + + + My Bookings + + + View and manage your freight booking requests. + + -
-
- - + setSearchTerm(e.target.value)} - className="pl-8!" + onChange={(e) => setSearchTerm(e.currentTarget.value)} + leftSection={} + radius="md" + className="w-full sm:w-80" + styles={{ input: { background: "white" } }} /> -
- - - - -
+
+
-
- - -
-

Total Bookings

-

- {bookings.length} -

-
-
- -
-
-
+ {/* ── Stat cards ──────────────────────────────────────────── */} + + } + gradient="from-emerald-500 to-emerald-700 shadow-emerald-500/30" + /> + } + gradient="from-sky-500 to-blue-600 shadow-sky-500/30" + /> + } + gradient="from-amber-400 to-orange-500 shadow-amber-500/30" + /> + - - -
-

Active Bookings

-

- {activeCount} -

-
-
- -
-
-
- - - -
-

Pending Approval

-

- {pendingCount} -

-
-
- -
-
-
-
- - - -
- Recent Requests - + {/* ── Table ───────────────────────────────────────────────── */} + + + + Recent Requests + A list of your recent freight bookings and their statuses. - -
- - -
+ - - {total === 0 && dataTableStatus === "success" ? ( -
- -

No bookings found

-

- {searchTerm ? "No bookings match your current search filter." : "You haven't requested any bookings yet."} -

-
- ) : ( - navigate(`/bookings/${(row as Freight.IBooking).id}`)} - pagination={{ - pageIndex: pagination.pageIndex, - pageSize: pagination.pageSize, - pageCount: pageCount, - totalCount: total, - }} - tableOptions={{ - state: { pagination }, - onPaginationChange: setPagination, - }} - containerClassName="border-b shadow-none" - footer={DataTableFooter} - /> - )} -
+ {total === 0 && dataTableStatus === "success" ? ( + + + + + + No bookings found + + + {searchTerm + ? "No bookings match your current search filter." + : "You haven't requested any bookings yet."} + + {!searchTerm && ( + + )} + + ) : ( + navigate(`/bookings/${(row as Freight.IBooking).id}`)} + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageCount: pageCount, + totalCount: total, + }} + tableOptions={{ + state: { pagination }, + onPaginationChange: setPagination, + }} + containerClassName="border-0 shadow-none" + footer={DataTableFooter} + /> + )}
-
-
+ + + ); +} + +function StatCard({ + label, + value, + icon, + gradient, +}: { + label: string; + value: number; + icon: React.ReactNode; + gradient: string; +}) { + return ( + + + + + {label} + + + {value} + + + + {icon} + + + ); } function StatusBadge({ status }: { status: string }) { - const styles: Record = { - DRAFT: "bg-amber-100 text-amber-700", - CONFIRMED: "bg-primary/10 text-primary", - IN_TRANSIT: "bg-muted text-foreground", - DELIVERED: "bg-primary/10 text-primary", - CANCELLED: "bg-destructive/10 text-destructive", + const colorMap: Record = { + DRAFT: "amber", + CONFIRMED: "edr-green", + IN_TRANSIT: "blue", + DELIVERED: "edr-green", + CANCELLED: "red", }; return ( - - {status.replace(/_/g, ' ')} - + {status.replace(/_/g, " ")} + ); } diff --git a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx index 78ba91af6..eb088c883 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -3,14 +3,8 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; -import { - AlertCircle, - Check, - ChevronLeft, - ChevronRight, - LoaderCircle, -} from "lucide-react"; -import { Button } from "@edr/ui-common"; +import { AlertCircle, Check, ChevronLeft, ChevronRight } from "lucide-react"; +import { Alert, Box, Button, Text } from "@mantine/core"; import { api } from "@/services/api"; import type { CreateBookingPayload } from "@/services/bookings.service"; import { @@ -210,27 +204,34 @@ export default function NewBookingPage() { className="flex flex-col" onSubmit={handleSubmit} > -
-
+ {/* Step indicator — sticky */} + + -
-
+ + -
-
+ {/* Step content */} + + {createMutation.isError && ( -
- -
-

Failed to save draft

-

- {createMutation.error instanceof Error - ? createMutation.error.message - : "An unexpected error occurred. Please try again."} -

-
-
+ } + radius="md" + mb="lg" + > + + Failed to save draft + + + {createMutation.error instanceof Error + ? createMutation.error.message + : "An unexpected error occurred. Please try again."} + + )} + {step === 1 && } {step === 2 && } {step === 3 && ( @@ -251,43 +252,49 @@ export default function NewBookingPage() { {step === 5 && ( )} -
-
+ + -
-
+ {/* Navigation footer — sticky */} + + + {step < STEPS.length ? ( - ) : ( )} -
-
+ + ); } diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/StepIndicator.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/StepIndicator.tsx index 76ef8e30d..d7ca51476 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/StepIndicator.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/StepIndicator.tsx @@ -9,19 +9,19 @@ export function StepIndicator({ step }: { step: number }) {
item.id - ? "bg-primary text-primary-foreground" + ? "bg-emerald-600 text-white shadow-sm shadow-emerald-600/40" : step === item.id - ? "border-2 border-primary text-primary" - : "bg-muted text-muted-foreground" + ? "border-2 border-emerald-500 text-emerald-600 shadow-sm shadow-emerald-500/30" + : "bg-gray-100 text-gray-400" }`} > {step > item.id ? : item.id}
= item.id ? "text-foreground" : "text-muted-foreground" + className={`hidden text-[10px] font-medium lg:block transition-colors ${ + step >= item.id ? "text-gray-800" : "text-gray-400" }`} > {item.short} @@ -29,8 +29,8 @@ export function StepIndicator({ step }: { step: number }) {
{index < STEPS.length - 1 && (
item.id ? "bg-primary" : "bg-border" + className={`mx-1 h-0.5 flex-1 rounded-full transition-all duration-300 ${ + step > item.id ? "bg-emerald-500" : "bg-gray-200" }`} /> )} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx index a731d081c..4d5f8e33a 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx @@ -1,31 +1,16 @@ import type { ReactNode } from "react"; -import type { - ControllerRenderProps, - FieldError as RhfFieldError, -} from "react-hook-form"; -import { - AlertTriangle, - Check, - CheckCircle2, - Info, - XCircle, -} from "lucide-react"; -import { - Field, - FieldDescription, - FieldError, - FieldLabel, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@edr/ui-common"; -import type { BookingFormInputValues, BookingFormValues } from "./schema"; -import { cn } from "@/lib/utils"; +import type { ControllerRenderProps, FieldError as RhfFieldError } from "react-hook-form"; +import { AlertTriangle, Check, CheckCircle2, Info, XCircle } from "lucide-react"; +import { Alert, Select, Text, Title } from "@mantine/core"; +import type { BookingFormInputValues } from "./schema"; export function OptionFieldError({ error }: { error?: { message?: string } }) { - return ; + if (!error?.message) return null; + return ( + + {error.message} + + ); } export function OptionCard({ @@ -44,16 +29,17 @@ export function OptionCard({ type="button" onClick={onClick} disabled={disabled} - className={`relative w-full rounded-xl border-2 p-4 text-left transition ${disabled - ? "cursor-not-allowed border-border bg-muted opacity-60" + className={`relative w-full rounded-xl border-2 p-4 text-left transition-all duration-150 ${ + disabled + ? "cursor-not-allowed border-gray-200 bg-gray-100 opacity-60" : selected - ? "border-primary bg-primary/5" - : "border-border bg-card hover:border-primary/40" - }`} + ? "border-emerald-500 bg-emerald-50 shadow-sm shadow-emerald-500/20" + : "border-gray-200 bg-white hover:border-emerald-300 hover:shadow-sm" + }`} > {selected && !disabled && ( - - + + )} {children} @@ -68,34 +54,25 @@ export function AlertBox({ tone: "warning" | "error" | "success" | "info"; children: ReactNode; }) { - const styles = { - warning: "bg-amber-50 border-amber-200 text-amber-800", - error: "bg-red-50 border-red-200 text-red-800", - success: "bg-emerald-50 border-emerald-200 text-emerald-800", - info: "bg-sky-50 border-sky-200 text-sky-800", + const map: Record = { + warning: { color: "yellow", icon: }, + error: { color: "red", icon: }, + success: { color: "teal", icon: }, + info: { color: "blue", icon: }, }; - const icons = { - warning: , - error: , - success: , - info: , - }; - + const { color, icon } = map[tone]; return ( -
- {icons[tone]} -
{children}
-
+ + {children} + ); } export function StepLabel({ children }: { children: ReactNode }) { return ( -

+ {children} -

+ ); } @@ -108,8 +85,12 @@ export function StepHeader({ }) { return (
-

{title}

-

{description}

+ + {title} + + + {description} +
); } @@ -120,46 +101,27 @@ export function SelectField({ label, placeholder, disabled, - children, + data, }: { field: ControllerRenderProps; error?: RhfFieldError; label: string; placeholder: string; disabled?: boolean; - children: ReactNode; + data: string[] | { value: string; label: string }[]; }) { return ( - - {label} - - - - ); -} - -export { SelectItem }; - -export function SelectOptions({ options }: { options: readonly string[] }) { - return ( - <> - {options.map((option) => ( - - {option} - - ))} - + - - + )} /> )}
+ {/* Last Mile */}
(
- +
-

- Last Mile - Delivery -

-

+

Last Mile — Delivery

+

Truck delivery from the destination rail yard to the final address (Port to Door).

@@ -194,7 +173,8 @@ export function Step2ServiceType({ form }: { form: BookingForm }) {
{ + onChange={(e) => { + const value = e.currentTarget.checked; field.onChange(value); if (!value) { form.setValue("lastMile.deliveryAddress", "", { @@ -206,6 +186,7 @@ export function Step2ServiceType({ form }: { form: BookingForm }) { }); } }} + color="edr-green" />
)} @@ -215,19 +196,19 @@ export function Step2ServiceType({ form }: { form: BookingForm }) { name="lastMile.deliveryAddress" control={form.control} render={({ field, fieldState }) => ( - - - - + )} /> )}
+ {/* Equipment Return */} {lastMileEnabled && (
(
-
-
-

Equipment Return

-

- {field.value === "with_return" - ? "Container returned to EDR after unloading." - : "Container retained by the customer after delivery."} -

-
+
+

Equipment Return

+

+ {field.value === "with_return" + ? "Container returned to EDR after unloading." + : "Container retained by the customer after delivery."} +

{ + onChange={(e) => { field.onChange( - value ? "with_return" : "without_return", + e.currentTarget.checked ? "with_return" : "without_return", ); }} + color="edr-green" />
)} @@ -259,6 +239,7 @@ export function Step2ServiceType({ form }: { form: BookingForm }) {
)} + {/* Customs Clearing */}
(
- +
-

- Customs Clearing Service -

-

+

Customs Clearing Service

+

EDR handles customs documentation and clearance on your behalf.

@@ -279,7 +258,8 @@ export function Step2ServiceType({ form }: { form: BookingForm }) {
field.onChange(e.currentTarget.checked)} + color="edr-green" />
)} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step4-route.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step4-route.tsx index f42e19449..c72d62d79 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step4-route.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step4-route.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo } from "react"; import { Controller, type UseFormReturn } from "react-hook-form"; import { Flame, MapPin, Snowflake } from "lucide-react"; -import { Field, SelectItem, Separator, Skeleton, Switch } from "@edr/ui-common"; +import { Divider, Skeleton, Stack, Switch } from "@mantine/core"; import type { Freight } from "@edr/types"; import { BookingFormInputValues, @@ -10,11 +10,7 @@ import { } from "./schema"; import { SelectField, StepHeader, StepLabel } from "./shared"; -type BookingForm = UseFormReturn< - BookingFormInputValues, - any, - BookingFormValues ->; +type BookingForm = UseFormReturn; export function Step4Route({ form, @@ -30,26 +26,29 @@ export function Step4Route({ const yardOptions = useMemo(() => { if (!referenceData?.yard) return []; - return referenceData.yard.map((y) => ({ - value: y.name, - label: y.name, - country: y.country, - })); + return referenceData.yard.map((y) => ({ value: y.name, label: y.name })); }, [referenceData]); const shippingLineOptions = useMemo(() => { if (!referenceData?.shipping_line) return []; - return referenceData.shipping_line.map((sl) => ({ - value: sl.name, - label: sl.name, - })); + return referenceData.shipping_line.map((sl) => ({ value: sl.name, label: sl.name })); }, [referenceData]); + const originData = useMemo( + () => yardOptions.filter((o) => o.value !== destinationYard), + [yardOptions, destinationYard], + ); + const destData = useMemo( + () => yardOptions.filter((o) => o.value !== originYard), + [yardOptions, originYard], + ); + const direction = getRouteDirection(originYard, destinationYard); + const directionStyle: Record = { export: "bg-sky-50 text-sky-800 border-sky-200", import: "bg-amber-50 text-amber-800 border-amber-200", - domestic: "bg-muted text-muted-foreground border-border", + domestic: "bg-gray-100 text-gray-600 border-gray-200", }; const directionLabel: Record = { export: "Export workflow (inside country to outside country)", @@ -85,15 +84,11 @@ export function Step4Route({ - - + data={originData} + /> )} /> - - + data={destData} + /> )} />
@@ -126,7 +117,7 @@ export function Step4Route({
)} - {direction && direction != "domestic" && ( + {direction && direction !== "domestic" && ( - {shippingLineOptions.map((sl) => ( - - {sl.label} - - ))} - + data={shippingLineOptions} + /> )} /> )} - -
+ + +

Hazardous Material

-

+

Applies a Hazard Surcharge to the final bill.

- + field.onChange(e.currentTarget.checked)} + color="edr-green" + />
)} /> @@ -176,13 +167,17 @@ export function Step4Route({

Refrigerated Cargo

-

+

Temperature-controlled transport applies a Refrigerator Surcharge.

- + field.onChange(e.currentTarget.checked)} + color="edr-green" + />
)} /> @@ -193,48 +188,18 @@ export function Step4Route({ function LoadingSkeleton() { return ( -
+
-
- - -
-
- - -
+ + + + + + + +
- +
); } - -function YardSelectOptions({ - options, - excludeValue, -}: { - options: Array<{ value: string; label: string; country: string }>; - excludeValue: string; -}) { - if (options.length === 0) { - return ( - - No yards available - - ); - } - - const availableOptions = options.filter( - (option) => option.value !== excludeValue, - ); - - return ( - <> - {availableOptions.map((option) => ( - - {option.label} - - ))} - - ); -} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step5-cargo-details.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step5-cargo-details.tsx index cbd70f5ac..5f2b428f5 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step5-cargo-details.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step5-cargo-details.tsx @@ -1,14 +1,7 @@ import { useMemo } from "react"; import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form"; import { MapPin, Package, Plus, Trash2, Weight } from "lucide-react"; -import { - Button, - Field, - FieldError, - FieldLabel, - Input, - Skeleton, -} from "@edr/ui-common"; +import { ActionIcon, Button, Skeleton, Stack, Text, TextInput } from "@mantine/core"; import type { Freight } from "@edr/types"; import { BookingFormInputValues, @@ -19,17 +12,13 @@ import { import { AlertBox, OptionCard, + OptionFieldError, SelectField, - SelectItem, StepHeader, StepLabel, } from "./shared"; -type BookingForm = UseFormReturn< - BookingFormInputValues, - any, - BookingFormValues ->; +type BookingForm = UseFormReturn; export function Step5CargoDetails({ form, @@ -44,7 +33,6 @@ export function Step5CargoDetails({ }) { const cargoType = form.watch("cargoType"); const freightType = form.watch("freightType"); - const bulkCommoditytype = form.watch("bulkCommoditytype"); const containers = form.watch("containers"); const { fields, append, remove } = useFieldArray({ @@ -61,9 +49,7 @@ export function Step5CargoDetails({ const freightTypeGroups = useMemo(() => { if (!referenceData?.cargo_type) return []; - return referenceData.cargo_type.filter( - (g) => g.code !== "CONTAINER", - ); + return referenceData.cargo_type.filter((g) => g.code !== "CONTAINER"); }, [referenceData]); const commodityOptions = useMemo(() => { @@ -97,14 +83,14 @@ export function Step5CargoDetails({ title="Cargo Details" description="Define your cargo type, weight, and container configuration." /> -
- +
+
- - + +
- - + +
); @@ -117,28 +103,27 @@ export function Step5CargoDetails({ description="Define your cargo type, weight, and container configuration." /> + {/* Cargo Type */}
Cargo Type * ( - +
{ field.onChange("container"); - form.setValue("freightType", "", { - shouldDirty: true, - }); + form.setValue("freightType", "", { shouldDirty: true }); }} > -
- +
+

Containerized

-

+

Pre-packed containerized cargo (20ft / 40ft).

@@ -153,45 +138,41 @@ export function Step5CargoDetails({

General Cargo

-

+

Bulk commodities or break-bulk cargo.

- - + +
)} />
+ {/* Weight */}
Weight ( - - - Total Cargo Weight(Tons)* - -
- - -
- -
+ } + error={fieldState.error?.message} + radius="md" + min={0} + step={0.01} + /> )} />
+ + {/* Bulk freight type */} {cargoType === "bulk" && (
Freight Type * @@ -199,7 +180,7 @@ export function Step5CargoDetails({ name="freightType" control={form.control} render={({ field, fieldState }) => ( - +
{freightTypeGroups.map((group) => { const val = group.code.toLowerCase(); @@ -219,36 +200,30 @@ export function Step5CargoDetails({ ); })}
- - + +
)} /> {freightType && commodityOptions.length > 0 && ( -
- ( - - {commodityOptions.map((option) => ( - - {option} - - ))} - - )} - /> -
+ ( + + )} + /> )}
)} + {/* Container list */} {cargoType === "container" && ( <>
@@ -256,18 +231,14 @@ export function Step5CargoDetails({ Containers
@@ -280,25 +251,31 @@ export function Step5CargoDetails({ return (
+ + Container {index + 1} + {fields.length > 1 && ( - + + )}
+ {/* Container size */} ( - +
{[ { @@ -321,52 +298,47 @@ export function Step5CargoDetails({ onClick={() => typeField.onChange(ct.val)} >
- +

{ct.label}

-

- {ct.limit} -

+

{ct.limit}

))}
- - + +
)} /> + {/* Qty + VGM + Type */}
( - - Quantity * +
+ + Quantity * +
- - qtyField.onChange(e.target.value) - } + onChange={(e) => qtyField.onChange(e.target.value)} onBlur={qtyField.onBlur} type="number" - aria-invalid={fieldState.invalid} - className="text-center" - min="1" + min={1} + className="h-9 w-full rounded-lg border border-gray-200 bg-white px-3 text-center text-sm outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500" />
- - + {fieldState.error?.message && ( + + {fieldState.error.message} + + )} +
)} /> @@ -389,20 +365,18 @@ export function Step5CargoDetails({ name={`containers.${index}.vgm`} control={form.control} render={({ field: vgmField, fieldState }) => ( - - Tons* - vgmField.onChange(e.target.value)} - onBlur={vgmField.onBlur} - type="number" - aria-invalid={fieldState.invalid} - placeholder="e.g. 18.5" - min="0" - step="0.1" - /> - - + vgmField.onChange(e.target.value)} + onBlur={vgmField.onBlur} + type="number" + label="Tons *" + placeholder="e.g. 18.5" + error={fieldState.error?.message} + radius="md" + min={0} + step={0.1} + /> )} /> @@ -415,13 +389,8 @@ export function Step5CargoDetails({ error={fieldState.error} label="Container Type *" placeholder="Select type..." - > - {containerTypeOptions.map((option) => ( - - {option} - - ))} - + data={containerTypeOptions} + /> )} />
@@ -441,18 +410,13 @@ export function Step5CargoDetails({ if (result.hasOddUnit) { return ( -
-
-

Unpaired 20ft Container

-

- One 20ft container occupies only half a wagon. The wagon - will depart once a co-loader is found to fill the - remaining slot, which{" "} - may delay departure beyond the standard - lead time. -

-
-
+

Unpaired 20ft Container

+

+ One 20ft container occupies only half a wagon. The wagon + will depart once a co-loader is found to fill the remaining + slot, which may delay departure beyond the + standard lead time. +

); } diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step8-review.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step8-review.tsx index 2c1b2e4d0..3d2e087db 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step8-review.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step8-review.tsx @@ -1,15 +1,5 @@ import { Controller, type UseFormReturn } from "react-hook-form"; -import { Check } from "lucide-react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, - Field, - FieldError, - FieldLabel, - Textarea, -} from "@edr/ui-common"; +import { Box, Card, Checkbox, SimpleGrid, Text, Textarea, Title } from "@mantine/core"; import { BookingFormInputValues, type BookingFormValues, @@ -17,11 +7,7 @@ import { } from "./schema"; import { StepHeader } from "./shared"; -type BookingForm = UseFormReturn< - BookingFormInputValues, - any, - BookingFormValues ->; +type BookingForm = UseFormReturn; export function Step8Review({ form, @@ -47,13 +33,17 @@ export function Step8Review({ return (
-

{label}

-

{value || "-"}

+ + {label} + + + {value || "—"} +
@@ -64,26 +54,53 @@ export function Step8Review({ const containerSummary = values.cargoType === "container" && values.containers.length > 0 ? values.containers - .filter((c) => +c.qty > 0) - .map((c) => `${c.qty} × ${c.type}`) - .join(", ") + .filter((c) => +c.qty > 0) + .map((c) => `${c.qty} × ${c.type}`) + .join(", ") : ""; + const totalVgm = values.cargoType === "container" ? values.containers.reduce( - (sum, c) => sum + (+c.qty || 0) * (+c.vgm || 0), - 0, - ) + (sum, c) => sum + (+c.qty || 0) * (+c.vgm || 0), + 0, + ) : 0; const cargoValue = values.cargoType === "container" ? containerSummary : values.freightType === "bulk" - ? `Bulk - ${values.bulkCommodity === "Others" ? values.bulkCommodityOther : values.bulkCommodity}` + ? `Bulk — ${values.bulkCommoditytype}` : values.freightType === "break_bulk" - ? `Break-Bulk - ${values.breakBulkType === "Others" ? values.breakBulkTypeOther : values.breakBulkType}` + ? `Break-Bulk` : ""; + + function ReviewCard({ + title, + children, + }: { + title: string; + children: React.ReactNode; + }) { + return ( + + + + {title} + + + + {children} + + + ); + } + return (
-
- - - - Contract & Service - - - - - - - - - - - - First & Last Mile - - - - - - - - - - - - - - Route & Cargo - - - - ${values.destinationYard}`} - target={3} - /> - + + + - - - - - + } + target={2} + /> + - - - - Container & Wagons - - - - - 0 ? `${totalVgm.toFixed(1)} tons` : ""} - target={4} - /> - - -
+ + + + + + + + + + + + + + + + + + 0 ? `${totalVgm.toFixed(1)} tons` : ""} + target={4} + /> + + ( - - Additional Notes -