From 08b46c306fdd0681e247cfad2a3b6b408cf71e63 Mon Sep 17 00:00:00 2001 From: Marshal Date: Sat, 6 Jun 2026 21:17:20 +0000 Subject: [PATCH] ui improvemnt --- apps/edr-freight-api/src/main.ts | 7 +- .../components/bookings/ApprovalStepsCard.tsx | 198 +++--- .../bookings/BookingActionsMenu.tsx | 342 +++++------ .../bookings/BookingActionsToolbar.tsx | 110 ++-- .../bookings/BookingConfirmDialog.tsx | 247 ++++---- .../bookings/BookingPricingSummary.tsx | 119 ++-- .../components/bookings/BookingStatGrid.tsx | 146 +++-- .../components/bookings/BookingStatusTabs.tsx | 12 +- .../bookings/BookingWorkflowStepper.tsx | 191 +++--- .../components/bookings/NextStepBanner.tsx | 35 +- .../bookings/detail/BookingApprovalCard.tsx | 68 +++ .../bookings/detail/BookingCargoCard.tsx | 63 ++ .../bookings/detail/BookingContainersCard.tsx | 60 ++ .../detail/BookingContractSummaryCard.tsx | 27 + .../bookings/detail/BookingDetailHeader.tsx | 76 +++ .../bookings/detail/BookingDetailToolbar.tsx | 37 ++ .../bookings/detail/BookingDocumentsCard.tsx | 69 +++ .../bookings/detail/BookingFactsCard.tsx | 57 ++ .../detail/BookingLifecycleStepper.tsx | 100 +++ .../detail/BookingMileServicesCard.tsx | 31 + .../bookings/detail/BookingPaymentCard.tsx | 43 ++ .../bookings/detail/BookingRequestHero.tsx | 111 ++++ .../detail/BookingReviewNotesCard.tsx | 49 ++ .../bookings/detail/BookingRouteCard.tsx | 69 +++ .../detail/BookingRouteServiceCard.tsx | 101 ++++ .../components/bookings/detail/MetricTile.tsx | 32 + .../bookings/detail/SectionCard.tsx | 32 + .../bookings/detail/booking-detail.styles.ts | 140 +++++ .../src/components/bookings/detail/index.ts | 18 + .../layout/FreightDashboardHeader.tsx | 231 +++---- .../layout/FreightDashboardLayout.tsx | 107 ++-- .../src/components/layout/FreightSidebar.tsx | 323 +++++----- .../src/pages/bookings/BookingDetailPage.tsx | 154 ++++- .../bookings/BookingRequestDetailPage.tsx | 572 ++++-------------- .../pages/bookings/BookingRequestsPage.tsx | 358 ++++++----- 35 files changed, 2731 insertions(+), 1604 deletions(-) create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingApprovalCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingCargoCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingContainersCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingContractSummaryCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingDetailHeader.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingDetailToolbar.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingDocumentsCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingFactsCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingLifecycleStepper.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingMileServicesCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingPaymentCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingRequestHero.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingReviewNotesCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingRouteCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingRouteServiceCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/MetricTile.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/SectionCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/index.ts diff --git a/apps/edr-freight-api/src/main.ts b/apps/edr-freight-api/src/main.ts index 0257eb4c1..a76378b0c 100644 --- a/apps/edr-freight-api/src/main.ts +++ b/apps/edr-freight-api/src/main.ts @@ -18,6 +18,7 @@ async function bootstrap() { // freight portal (5173), passenger portal (5174), backoffices (5183/5184) // and any other dev port can call the API with cookies + Authorization. // For production, restrict `origin` to known FQDNs. + app.enableCors({ origin: true, // reflect request origin credentials: true, @@ -52,8 +53,10 @@ async function bootstrap() { SwaggerModule.setup("api/docs", app, document); const port = parseInt(process.env.PORT ?? "3001", 10); - await app.listen(port, "0.0.0.0"); - // await app.listen(port) + // await app.listen(port, "0.0.0.0"); + await app.listen( + + port) // eslint-disable-next-line no-console console.log(`[freight-api] listening on port ${port}`); } diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/ApprovalStepsCard.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/ApprovalStepsCard.tsx index 293c17e2a..ebfc573b0 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/ApprovalStepsCard.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/ApprovalStepsCard.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from "react"; import { Check, ShieldCheck } from "lucide-react"; +import { Stack, Group, Text, Badge, Button, Box } from "@mantine/core"; import { BookingConfirmDialog } from "./BookingConfirmDialog"; import { useAuth } from "@/auth/useAuth"; @@ -11,9 +12,7 @@ import { } from "@/features/bookings/booking-actions.config"; import type { useBookingMutations } from "@/hooks/bookings/useBookings"; import type { BookingApprovalStep, BookingDetail } from "@/types/booking"; -import { bookingGlass, bookingSurface } from "./booking-ui.styles"; -import { Badge, Button } from "@edr/ui-common"; -import { cn } from "@/lib/utils"; +import { SectionCard } from "./detail/SectionCard"; type Mutations = ReturnType; @@ -26,23 +25,16 @@ interface ApprovalStepsCardProps { export function ApprovalStepsCard({ booking, mutations }: ApprovalStepsCardProps) { const { user } = useAuth(); const [confirmOpen, setConfirmOpen] = useState(false); - const [pendingStep, setPendingStep] = useState( - null, - ); + const [pendingStep, setPendingStep] = useState(null); const steps = useMemo( - () => - [...(booking.approvalSteps ?? [])].sort( - (a, b) => a.stepOrder - b.stepOrder, - ), + () => [...(booking.approvalSteps ?? [])].sort((a, b) => a.stepOrder - b.stepOrder), [booking.approvalSteps], ); const nextPending = getNextPendingApprovalStep(steps); const summary = formatApprovalProgress(booking.status, steps); - const pendingAction = pendingStep - ? buildApproveActionForStep(pendingStep) - : null; + const pendingAction = pendingStep ? buildApproveActionForStep(pendingStep) : null; const openApprove = (step: BookingApprovalStep) => { setPendingStep(step); @@ -62,54 +54,60 @@ export function ApprovalStepsCard({ booking, mutations }: ApprovalStepsCardProps ); }; + const subtitle = + summary.detail || + (nextPending + ? `Next: ${nextPending.requiredRole} · step ${nextPending.stepOrder}` + : steps.length + ? "All steps complete" + : "Accept submission to begin"); + return ( <> -
-
-
- -
-
-

- Approval chain -

-

- {summary.detail || - (nextPending - ? `Next: ${nextPending.requiredRole} · step ${nextPending.stepOrder}` - : steps.length - ? "All steps complete" - : "Accept submission to begin")} -

-
-
+ + {steps.filter((s) => s.status === "APPROVED").length}/{steps.length} + + } + > + + {subtitle} + -
- {steps.length === 0 ? ( -

- Use{" "} - - Accept for approval - {" "} - in staff actions to instantiate steps. -

- ) : ( -
    - {steps.map((step) => ( - - ))} -
- )} -
-
+ {steps.length === 0 ? ( + + Use Accept for approval in staff actions to instantiate steps. + + ) : ( + + {steps.map((step) => ( + + ))} + + )} + void; }) { const canApprove = canActOnApprovalStep(user, step, steps); - const statusStyles = + const statusColor = step.status === "APPROVED" - ? "border-emerald-500/25 bg-emerald-500/10 text-black" + ? "green" : step.status === "REJECTED" - ? "bg-red-500/10 text-red-800 dark:text-red-300" + ? "red" : isNext - ? "border-emerald-500/25 bg-emerald-500/10 text-black" - : "bg-muted/40 text-muted-foreground"; + ? "green" + : "gray"; + const highlighted = isNext || step.status === "APPROVED"; return ( -
  • -
    - + {step.stepOrder} - -
    -

    + + + {step.requiredRole} -

    + {step.remarks && ( -

    + {step.remarks} -

    + )} -
    -
    -
    + + + {canApprove && ( )} - + {step.status} -
    -
  • + + ); } diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsMenu.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsMenu.tsx index e4e1d8db9..49950d97b 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsMenu.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsMenu.tsx @@ -1,10 +1,6 @@ import { useNavigate } from "react-router-dom"; -import { - ChevronRight, - ExternalLink, - Loader2, - MoreHorizontal, -} from "lucide-react"; +import { ChevronRight, ExternalLink, MoreHorizontal } from "lucide-react"; +import { Button, Menu, ActionIcon, Group, Text } from "@mantine/core"; import { BookingConfirmDialog } from "./BookingConfirmDialog"; import { useBookingActionDialog } from "./useBookingActionDialog"; @@ -16,16 +12,6 @@ import { type BookingActionContext, } from "@/features/bookings/booking-actions.config"; import type { BookingListRow } from "@/types/booking"; -import { cn } from "@/lib/utils"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@edr/ui-common"; interface BookingActionsMenuProps { row: BookingListRow; @@ -39,7 +25,6 @@ interface BookingActionsMenuProps { export function BookingActionsMenu({ row, variant = "table", - className, onSuppressRowClick, }: BookingActionsMenuProps) { const navigate = useNavigate(); @@ -57,184 +42,179 @@ export function BookingActionsMenu({ const goToContract = () => navigate(`/dashboard/booking-requests/${row.id}/contract`); - const hasMenu = listRowHasActions(row, user); + const handleAction = (action: (typeof actions)[number]) => { + onSuppressRowClick?.(); + if (isContractNavAction(action.id)) { + goToContract(); + } else { + flow.openAction(action); + } + }; + const hasMenu = listRowHasActions(row, user); const primary = actions.find((a) => a.primary) ?? actions[0]; if (!hasMenu && variant === "table") { return ( - + + + ); + } + + // Toolbar: lay every action out as a button row. + if (variant === "toolbar" && actions.length > 0) { + return ( + <> + + {actions.map((action) => { + const Icon = action.icon; + const destructive = action.variant === "destructive"; + return ( + + ); + })} + + + ); } return ( - <> -
    e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - > - {variant === "table" && primary && ( - + )} + + + + - - {primary.shortLabel} - - )} - - {variant === "toolbar" && actions.length > 0 ? ( -
    - {actions.map((action) => { - const Icon = action.icon; - return ( - - ); - })} -
    - ) : ( - - - - - - + +
    +
    + + + {row.reference} - - - {actions.map((action) => { - const Icon = action.icon; - return ( - { - event.preventDefault(); - onSuppressRowClick?.(); - if (isContractNavAction(action.id)) { - goToContract(); - } else { - flow.openAction(action); - } - }} - > - - {action.label} - - ); - })} - {actions.length > 0 && } - { - event.preventDefault(); - onSuppressRowClick?.(); - navigate(`/dashboard/booking-requests/${row.id}`); - }} - > - - Open full details - - - - )} -
    + + + {actions.map((action) => { + const Icon = action.icon; + return ( + } + onClick={() => handleAction(action)} + > + {action.label} + + ); + })} + {actions.length > 0 && } + } + onClick={() => { + onSuppressRowClick?.(); + navigate(`/dashboard/booking-requests/${row.id}`); + }} + > + Open full details + + + - { - if (!open) onSuppressRowClick?.(); - flow.setDialogOpen(open); - }} - action={pendingAction} - reference={flow.mergedContext.reference} - inputValue={flow.inputValue} - onInputChange={flow.setInputValue} - selectedFile={flow.selectedFile} - onFileChange={flow.setSelectedFile} - onConfirm={() => { - onSuppressRowClick?.(); - flow.runAction(); - }} - isPending={mutations.isPending || flow.detailLoading} - confirmDisabled={flow.confirmDisabled} - extra={ - flow.detailLoading ? ( -

    - - Loading approval steps… -

    - ) : pendingAction?.id === "approve" && - !getNextPendingApprovalStep(flow.mergedContext.approvalSteps) ? ( -

    - No pending approval step. Refresh the page after staff accept, or - reject the booking. -

    - ) : null - } - /> - + + + ); +} + +function ActionDialog({ + flow, + pendingAction, + onSuppressRowClick, +}: { + flow: ReturnType; + pendingAction: ReturnType["pendingAction"]; + onSuppressRowClick?: () => void; +}) { + return ( + { + if (!open) onSuppressRowClick?.(); + flow.setDialogOpen(open); + }} + action={pendingAction} + reference={flow.mergedContext.reference} + inputValue={flow.inputValue} + onInputChange={flow.setInputValue} + selectedFile={flow.selectedFile} + onFileChange={flow.setSelectedFile} + onConfirm={() => { + onSuppressRowClick?.(); + flow.runAction(); + }} + isPending={flow.mutations.isPending || flow.detailLoading} + confirmDisabled={flow.confirmDisabled} + extra={ + flow.detailLoading ? ( + + Loading approval steps… + + ) : pendingAction?.id === "approve" && + !getNextPendingApprovalStep(flow.mergedContext.approvalSteps) ? ( + + No pending approval step. Refresh the page after staff accept, or reject the + booking. + + ) : null + } + /> ); } diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsToolbar.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsToolbar.tsx index 006c57e11..96f2b3829 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsToolbar.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsToolbar.tsx @@ -1,12 +1,11 @@ -import { Download, Zap } from "lucide-react"; +import { Download, Zap, FileText } from "lucide-react"; +import { Stack, Text, Button } from "@mantine/core"; import type { BookingDetail } from "@/types/booking"; import { BookingActionsMenu } from "./BookingActionsMenu"; -import { bookingSurface } from "./booking-ui.styles"; +import { SectionCard } from "./detail/SectionCard"; import { toBookingListRow } from "@/features/bookings/mapBookingListRow"; import type { useBookingMutations } from "@/hooks/bookings/useBookings"; -import { Button } from "@edr/ui-common"; -import { cn } from "@/lib/utils"; type Mutations = ReturnType; @@ -16,10 +15,7 @@ interface BookingActionsToolbarProps { } /** Detail-page actions: primary toolbar + downloads. */ -export function BookingActionsToolbar({ - booking, - mutations, -}: BookingActionsToolbarProps) { +export function BookingActionsToolbar({ booking, mutations }: BookingActionsToolbarProps) { const row = toBookingListRow(booking); const { status } = booking; @@ -33,50 +29,62 @@ export function BookingActionsToolbar({ URL.revokeObjectURL(url); }; - if ( - status === "REJECTED" || - status === "CANCELLED" || - status === "COMPLETED" - ) { + if (status === "REJECTED" || status === "CANCELLED" || status === "COMPLETED") { return null; } if (status === "CHANGES_REQUESTED") { return ( - - {booking.latestChangeRequestNote && ( -

    - {booking.latestChangeRequestNote} -

    - )} -
    + + + + No staff actions until resubmit. + + {booking.latestChangeRequestNote && ( + + {booking.latestChangeRequestNote} + + )} + + ); } if (["DRAFT", "PENDING_CONSOLIDATION", "CONSOLIDATED"].includes(status)) { return ( - + + + Monitor until the customer or system advances status. + + ); } return ( -
    - - - + + + + + Confirm each step before it is applied. + + + + {status === "CONTRACT_READY" && ( - + - + )} -
    - ); -} - -function PanelShell({ - title, - description, - children, - muted, -}: { - title: string; - description: string; - children: React.ReactNode; - muted?: boolean; -}) { - return ( -
    -
    -
    - -
    -
    -

    {title}

    -

    {description}

    -
    -
    -
    {children}
    -
    + ); } diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingConfirmDialog.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingConfirmDialog.tsx index 190fe4475..3eae09b49 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingConfirmDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingConfirmDialog.tsx @@ -1,17 +1,16 @@ -import { Loader2 } from "lucide-react"; +import type { ReactNode } from "react"; +import { + Modal, + Group, + Stack, + Text, + Box, + Button, + Textarea, + FileInput, +} from "@mantine/core"; import type { BookingActionDef } from "@/features/bookings/booking-actions.config"; -import { cn } from "@/lib/utils"; -import { - Button, - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - Textarea, -} from "@edr/ui-common"; interface BookingConfirmDialogProps { open: boolean; @@ -25,7 +24,7 @@ interface BookingConfirmDialogProps { onConfirm: () => void; isPending: boolean; confirmDisabled?: boolean; - extra?: React.ReactNode; + extra?: ReactNode; } export function BookingConfirmDialog({ @@ -45,129 +44,119 @@ export function BookingConfirmDialog({ if (!action || !action.confirmTitle) return null; const Icon = action.icon; - const needsTextInput = - action.input === "note" || action.input === "reason"; + const needsTextInput = action.input === "note" || action.input === "reason"; const needsFileInput = action.input === "file"; const inputMissing = - (needsTextInput && !inputValue.trim()) || - (needsFileInput && !selectedFile); + (needsTextInput && !inputValue.trim()) || (needsFileInput && !selectedFile); const isDestructive = action.variant === "destructive"; - - const preventClickThrough = (event: React.MouseEvent) => { - event.preventDefault(); - }; + const accent = isDestructive ? "red" : "green"; return ( - - event.preventDefault()} + onOpenChange(false)} + withCloseButton={false} + centered + radius="md" + size="md" + padding={0} + title={null} + > + {/* Header */} + -
    - -
    -
    - -
    -
    - - {action.confirmTitle} - - {reference && ( -

    - {reference} -

    - )} -
    -
    - - {action.confirmDescription} - -
    -
    - -
    - {needsTextInput && ( -
    - -