From f3284a13d099f7252c50d2ffc3a444803f87f96f Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Tue, 16 Jun 2026 17:25:29 +0300 Subject: [PATCH 1/7] refactor(workflow): Revamp booking progress stages and status mapping --- .../portal/src/pages/MyPortalPage.tsx | 12 +- .../components/StatusHero.tsx | 61 +++++---- .../BookingDetailPage/components/pricing.tsx | 24 ++-- .../bookings/BookingDetailPage/constants.ts | 70 +++++----- .../src/pages/bookings/NewBookingPage.tsx | 123 +++++++++++++++--- 5 files changed, 195 insertions(+), 95 deletions(-) diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx index a6e34b7ab..197ba3a44 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx @@ -488,7 +488,7 @@ export default function MyPortalPage() { icon={Clock3} label="Awaiting Payment" value={outstandingInvoices.length.toString()} - delta={`${formatCurrency(totalOutstanding || 377500, "ETB")} due`} + delta={`${formatCurrency(totalOutstanding || 0, "ETB")} due`} deltaColor="edr-amber-text" divider /> @@ -506,9 +506,9 @@ export default function MyPortalPage() { value={ dashboard ? formatCurrency( - dashboard.spendYtd, - dashboard.spendCurrency as Currency, - ) + dashboard.spendYtd, + dashboard.spendCurrency as Currency, + ) : "—" } delta={ @@ -693,7 +693,9 @@ export default function MyPortalPage() { ) : ( <> - {(dashboard?.freightVolume.totalTonnes ?? 0).toLocaleString()}{" "} + {( + dashboard?.freightVolume.totalTonnes ?? 0 + ).toLocaleString()}{" "} t diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/StatusHero.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/StatusHero.tsx index 95e4cdf78..2ba282fa2 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/StatusHero.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/StatusHero.tsx @@ -14,7 +14,7 @@ export function StatusHero({ booking: Freight.IBooking; children?: React.ReactNode; }) { - const status = booking.status as string; + const status = booking.status; const cfg = STATUS_MAP[status] ?? STATUS_MAP.DRAFT; const negative = isNegative(status); const draft = isDraftLike(status); @@ -45,7 +45,12 @@ export function StatusHero({
@@ -77,7 +82,7 @@ export function StatusHero({ > {chipLabel}
- + {chipValue} @@ -114,8 +119,13 @@ function ProgressTracker({ return ( /* Scrollable on mobile so 5 stages never overflow */
{PROGRESS_STAGES.map((stage, idx) => { @@ -131,14 +141,15 @@ function ProgressTracker({ : state === "active" ? activeFill : "#0EA371"; - const circleBorder = state === "idle" ? "1px solid #E1E7EE" : undefined; + const circleBorder = + state === "idle" ? "1px solid #E1E7EE" : undefined; const circleShadow = state === "active" ? `0 0 0 4px ${activeRing}` : undefined; return (
{/* left connector */} @@ -147,15 +158,19 @@ function ProgressTracker({ style={{ height: 3, background: - idx === 0 ? "transparent" : reachedLeft ? "#0EA371" : "#E1E7EE", + idx === 0 + ? "transparent" + : reachedLeft + ? "#0EA371" + : "#E1E7EE", }} /> {/* stage circle */}
{stage.label} - - {state === "done" - ? "Completed" - : state === "active" - ? negative - ? "Stopped" - : "In progress" - : "Pending"} -
); })} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/pricing.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/pricing.tsx index ec6c10f02..1fec98a35 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/pricing.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/pricing.tsx @@ -1,5 +1,5 @@ -import { Box, Button, Group, Stack, Text } from "@mantine/core"; -import { CheckCircle2, Clock, FileText } from "lucide-react"; +import { Box, Group, Stack, Text } from "@mantine/core"; +import { CheckCircle2, Clock } from "lucide-react"; import type { Freight } from "@edr/types"; @@ -173,16 +173,16 @@ export function PaymentCard({ )} - + {/* */} ); } diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/constants.ts b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/constants.ts index c6e19fb9b..6ff19a633 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/constants.ts +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/constants.ts @@ -3,6 +3,7 @@ import { FileText, PackageCheck, ShieldCheck, + Ship, Train, } from "lucide-react"; @@ -15,32 +16,41 @@ export const PROGRESS_STAGES = [ { label: "Submitted", icon: ClipboardCheck, - statuses: ["SUBMITTED", "PENDING_APPROVAL"], + statuses: ["SUBMITTED"], }, { - label: "Approved", + label: "Approval", + icon: ShieldCheck, + statuses: ["PENDING_APPROVAL", "APPROVED_PENDING_SIGNATURE", "APPROVED"], + }, + { + label: "Contract", + icon: ShieldCheck, + statuses: ["CONTRACT_READY", "SIGNED_CUSTOMER"], + }, + { + label: "Payment", icon: ShieldCheck, statuses: [ - "APPROVED_PENDING_SIGNATURE", - "APPROVED", - "CONTRACT_READY", - "SIGNED_CUSTOMER", "FULLY_EXECUTED", + "SELECTED_FOR_BATCH", + "PAYMENT_VERIFICATION_IN_PROGRESS", + ], + }, + { + label: "Loading", + icon: Ship, + statuses: [ + "PAID", + "PNR_GENERATED", + "PENDING_CONSOLIDATION", + "CONSOLIDATED", ], }, { label: "In Transit", icon: Train, - statuses: [ - "SELECTED_FOR_BATCH", - "EXPIRED", - "PNR_GENERATED", - "PAYMENT_VERIFICATION_IN_PROGRESS", - "PAID", - "IN_TRANSIT", - "PENDING_CONSOLIDATION", - "CONSOLIDATED", - ], + statuses: ["EXPIRED", "IN_TRANSIT"], }, { label: "Complete", @@ -72,7 +82,7 @@ export const STATUS_MAP: Record< PENDING_APPROVAL: { title: "Pending approval", description: "Your booking is moving through the approval process.", - stage: 1, + stage: 2, }, APPROVED_PENDING_SIGNATURE: { title: "Approved — awaiting signature", @@ -88,71 +98,71 @@ export const STATUS_MAP: Record< title: "Contract ready to sign", description: "Your contract is ready. Review and apply your signature to proceed.", - stage: 2, + stage: 3, }, SIGNED_CUSTOMER: { title: "Signed — awaiting staff", description: "Your signature has been submitted. Awaiting the final staff signature.", - stage: 2, + stage: 3, }, FULLY_EXECUTED: { title: "Contract fully executed", description: "Signed by all parties. You can now proceed to payment.", - stage: 2, + stage: 4, }, SELECTED_FOR_BATCH: { title: "Selected for a train — payment due", description: "Your booking was selected for a scheduled train. Complete payment within the pay window to secure your slot.", - stage: 3, + stage: 4, }, EXPIRED: { title: "Pay window expired", description: "The payment window was missed. You can move this booking to another schedule or cancel it.", - stage: 3, + stage: 6, }, PNR_GENERATED: { title: "Payment reference generated", description: "A payment reference number has been generated for this booking.", - stage: 3, + stage: 5, }, PAYMENT_VERIFICATION_IN_PROGRESS: { title: "Verifying payment", description: "Your payment is being verified.", - stage: 3, + stage: 4, }, PAID: { title: "Payment confirmed", description: "Payment has been confirmed for this booking.", - stage: 3, + stage: 5, }, IN_TRANSIT: { title: "Cargo moving", description: "Your shipment is currently moving through the rail network.", - stage: 3, + stage: 6, }, PENDING_CONSOLIDATION: { title: "Pending consolidation", description: "Awaiting a consolidation partner shipment.", - stage: 3, + stage: 5, }, CONSOLIDATED: { title: "Consolidated", description: "Cargo has been consolidated with a partner shipment.", - stage: 3, + stage: 5, }, COMPLETED: { title: "Service complete", description: "Cargo delivered and service successfully terminated.", - stage: 4, + stage: 7, }, DELIVERED: { title: "Service complete", description: "Cargo delivered and service successfully terminated.", - stage: 4, + stage: 7, }, REJECTED: { title: "Booking rejected", 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 e992bdd20..696381917 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -3,7 +3,7 @@ import type { CreateBookingPayload } from "@/services/bookings.service"; import { zodResolver } from "@hookform/resolvers/zod"; import { Alert, Box, Button, Group, Text, Title } from "@mantine/core"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { AlertCircle, Check, ChevronLeft, ChevronRight } from "lucide-react"; +import { AlertCircle, Check, ChevronLeft, ChevronRight, Send } from "lucide-react"; import { useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; @@ -97,6 +97,31 @@ export default function NewBookingPage() { }, }); + const submitMutation = useMutation({ + mutationFn: async (payload: CreateBookingPayload) => { + const booking = await api.bookings.create.call(payload); + + const documents = form.getValues("documents") ?? {}; + const hasDocuments = Object.values(documents).some((value) => + Array.isArray(value) ? value.length > 0 : Boolean(value), + ); + if (hasDocuments) { + await api.bookings.uploadDocuments.call({ + id: booking.id, + files: documents, + }); + } + + await api.bookings.submit.call({ id: booking.id }); + + return booking; + }, + onSuccess: (booking) => { + queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() }); + navigate(`/bookings/${booking.id}`); + }, + }); + const form = useForm({ defaultValues: initialBookingFormValues, resolver: zodResolver(bookingFormSchema), @@ -116,6 +141,15 @@ export default function NewBookingPage() { return route; }, [originYard, destinationYard]); + const docValues = form.watch("documents") ?? {}; + const hasDocuments = useMemo( + () => + Object.values(docValues).some((value) => + Array.isArray(value) ? value.length > 0 : Boolean(value), + ), + [docValues], + ); + async function handleContinue() { const valid = await form.trigger(stepFields[step], { shouldFocus: true }); if (!valid) return; @@ -123,14 +157,14 @@ export default function NewBookingPage() { setStep((currentStep) => Math.min(STEPS.length, currentStep + 1)); } - const handleSubmit = form.handleSubmit((data) => { + function buildApiPayload(data: BookingFormValues): CreateBookingPayload { if (data.contractType === "renewal" && !data.previousContractRef) { form.setError("previousContractRef", { type: "manual", message: "Select a previous contract reference.", }); setStep(1); - return; + throw new Error("Validation failed"); } const totalWeight = @@ -141,7 +175,6 @@ export default function NewBookingPage() { ) : Number(data.cargoWeight || 0); - // ── Reference data lookups ────────────────────────────────────────── const shippingLines = referenceData?.shipping_line ?? []; const cargoTree = referenceData?.cargo_type ?? []; const containerGroups = referenceData?.containers ?? []; @@ -174,8 +207,7 @@ export default function NewBookingPage() { (s) => s.id === data.serviceTypeId, )!; - // ── Build API payload ─────────────────────────────────────────────── - const apiPayload: CreateBookingPayload = { + return { scheduledDate: new Date().toISOString(), contractType: data.contractType.toUpperCase() as CreateBookingPayload["contractType"], @@ -222,8 +254,24 @@ export default function NewBookingPage() { : {}), ...(cargoFreeText ? { cargoFreeText } : {}), }; + } - createMutation.mutate(apiPayload); + const handleDraftSubmit = form.handleSubmit((data) => { + try { + const apiPayload = buildApiPayload(data); + createMutation.mutate(apiPayload); + } catch { + // validation error already handled + } + }); + + const handleFullSubmit = form.handleSubmit((data) => { + try { + const apiPayload = buildApiPayload(data); + submitMutation.mutate(apiPayload); + } catch { + // validation error already handled + } }); return ( @@ -270,7 +318,7 @@ export default function NewBookingPage() { id="new-booking-form" className="flex flex-col" style={{ flex: 1 }} - onSubmit={handleSubmit} + onSubmit={handleDraftSubmit} > @@ -295,6 +343,24 @@ export default function NewBookingPage() { )} + {submitMutation.isError && ( + } + radius="md" + mb="lg" + > + + Failed to submit + + + {submitMutation.error instanceof Error + ? submitMutation.error.message + : "An unexpected error occurred. Please try again."} + + + )} + {step === 1 && ( )} @@ -367,18 +433,35 @@ export default function NewBookingPage() { Continue ) : ( - + + + {hasDocuments && ( + + )} + )} From c73780712d7009313a1febce23e77e22682c4b9b Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Tue, 16 Jun 2026 20:37:28 +0300 Subject: [PATCH 2/7] fixes --- packages/types/src/index.ts | 14 ++++++++++++-- pnpm-lock.yaml | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 397d240bf..c3b9319b3 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -2,5 +2,15 @@ export * from "./common/index"; export * from "./freight/index"; export * as Freight from "./freight/index"; export * as Passenger from "./passenger/index"; -export type { PaymentEvent, PaymentEventType, PaymentFailedEvent, PaymentSucceededEvent } from "./common/payments"; -export { PaymentIntentSnapshot, InitiatePaymentRequest, PaymentReferenceType, PaymentService } from "./common/payments"; +export type { + PaymentEvent, + PaymentEventType, + PaymentFailedEvent, + PaymentSucceededEvent, +} from "./common/payments"; +export { + type PaymentIntentSnapshot, + type InitiatePaymentRequest, + PaymentReferenceType, + PaymentService, +} from "./common/payments"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 750c9ec01..c1b29edac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -436,7 +436,7 @@ importers: version: 10.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/microservices': specifier: ^11.1.24 - version: 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/passport': specifier: ^10.0.3 version: 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) From 1861d45f1e166a598467042fb811b76ece36fc00 Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Tue, 16 Jun 2026 20:38:48 +0300 Subject: [PATCH 3/7] feat: add the payment to booking page --- .../src/pages/bookings/NewBookingPage.tsx | 159 +++++++++++++++--- .../new-booking-form/step8-review.tsx | 98 ++++++++++- 2 files changed, 233 insertions(+), 24 deletions(-) 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 696381917..6ccf34362 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -1,13 +1,27 @@ import { api } from "@/services/api"; -import type { CreateBookingPayload } from "@/services/bookings.service"; +import type { + CreateBookingPayload, + GeneratePriceResponse, +} from "@/services/bookings.service"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Alert, Box, Button, Group, Text, Title } from "@mantine/core"; +import { + Alert, + Box, + Button, + Group, + Modal, + Stack, + Text, + TextInput, + Title, +} from "@mantine/core"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { AlertCircle, Check, ChevronLeft, ChevronRight, Send } from "lucide-react"; +import { AlertCircle, Check, ChevronLeft, ChevronRight, Send, XCircle } from "lucide-react"; import { useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; import useAuth from "@/hooks/useAuth"; +import type { Freight } from "@/types"; import { BookingFormInputValues, STEPS, @@ -97,28 +111,55 @@ export default function NewBookingPage() { }, }); - const submitMutation = useMutation({ + const createAndPriceMutation = useMutation({ mutationFn: async (payload: CreateBookingPayload) => { const booking = await api.bookings.create.call(payload); const documents = form.getValues("documents") ?? {}; - const hasDocuments = Object.values(documents).some((value) => + const hasDocs = Object.values(documents).some((value) => Array.isArray(value) ? value.length > 0 : Boolean(value), ); - if (hasDocuments) { + if (hasDocs) { await api.bookings.uploadDocuments.call({ id: booking.id, files: documents, }); } - await api.bookings.submit.call({ id: booking.id }); + const pricing = await api.bookings.generatePrice.call({ id: booking.id }); - return booking; + return { bookingId: booking.id, pricing }; }, - onSuccess: (booking) => { + onSuccess: ({ bookingId, pricing }) => { + setPriceBookingId(bookingId); + setPricingData(pricing); + setPricingPhase("ready"); queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() }); - navigate(`/bookings/${booking.id}`); + }, + onError: () => { + setPricingPhase("idle"); + }, + }); + + const confirmMutation = useMutation({ + mutationFn: async () => { + if (!priceBookingId) throw new Error("No booking to confirm"); + await api.bookings.submit.call({ id: priceBookingId }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() }); + navigate(`/bookings/${priceBookingId}`); + }, + }); + + const abortMutation = useMutation({ + mutationFn: async (reason: string) => { + if (!priceBookingId) throw new Error("No booking to abort"); + await api.bookings.cancel.call({ id: priceBookingId, reason }); + }, + onSuccess: () => { + setCancelDialogOpen(false); + navigate("/bookings"); }, }); @@ -150,6 +191,12 @@ export default function NewBookingPage() { [docValues], ); + const [pricingPhase, setPricingPhase] = useState<"idle" | "generating" | "ready">("idle"); + const [pricingData, setPricingData] = useState(null); + const [priceBookingId, setPriceBookingId] = useState(null); + const [cancelDialogOpen, setCancelDialogOpen] = useState(false); + const [cancelReason, setCancelReason] = useState(""); + async function handleContinue() { const valid = await form.trigger(stepFields[step], { shouldFocus: true }); if (!valid) return; @@ -265,10 +312,11 @@ export default function NewBookingPage() { } }); - const handleFullSubmit = form.handleSubmit((data) => { + const handleGeneratePrice = form.handleSubmit((data) => { try { const apiPayload = buildApiPayload(data); - submitMutation.mutate(apiPayload); + setPricingPhase("generating"); + createAndPriceMutation.mutate(apiPayload); } catch { // validation error already handled } @@ -343,7 +391,7 @@ export default function NewBookingPage() { )} - {submitMutation.isError && ( + {createAndPriceMutation.isError && ( } @@ -351,11 +399,11 @@ export default function NewBookingPage() { mb="lg" > - Failed to submit + Failed to generate price estimate - {submitMutation.error instanceof Error - ? submitMutation.error.message + {createAndPriceMutation.error instanceof Error + ? createAndPriceMutation.error.message : "An unexpected error occurred. Please try again."} @@ -392,6 +440,15 @@ export default function NewBookingPage() { setStep={setStep} direction={direction!} referenceData={referenceData} + pricingPhase={pricingPhase} + pricingData={pricingData} + onConfirm={() => confirmMutation.mutate()} + onContinueLater={ + priceBookingId ? () => navigate(`/bookings/${priceBookingId}`) : undefined + } + onAbort={() => setCancelDialogOpen(true)} + confirmPending={confirmMutation.isPending} + abortPending={abortMutation.isPending} /> )} @@ -432,7 +489,7 @@ export default function NewBookingPage() { > Continue - ) : ( + ) : pricingPhase === "idle" ? ( )} - )} + ) : pricingPhase === "generating" ? ( + + ) : null} - {/* */} + + setCancelDialogOpen(false)} + title={Abort booking} + radius="lg" + centered + > + + + Are you sure you want to abort this booking? This action cannot be + undone. + + setCancelReason(e.currentTarget.value)} + radius="md" + data-autofocus + /> + + + + + + ); } 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 7d14dd28e..bbfd1cc34 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,5 +1,17 @@ import { Controller, type UseFormReturn } from "react-hook-form"; -import { Box, Card, Checkbox, SimpleGrid, Text, Textarea } from "@mantine/core"; +import { + Box, + Button, + Card, + Checkbox, + Divider, + Group, + Loader, + SimpleGrid, + Text, + Textarea, +} from "@mantine/core"; +import { Check, Send, XCircle } from "lucide-react"; import { BookingFormInputValues, BOOKING_DOCS_SETTING, @@ -8,6 +20,7 @@ import { } from "./schema"; import { StepHeader } from "./shared"; import type { Freight } from "@/types"; +import type { GeneratePriceResponse } from "@/services/bookings.service"; type BookingForm = UseFormReturn< BookingFormInputValues, @@ -20,11 +33,25 @@ export function Step8Review({ setStep, direction, referenceData, + pricingPhase = "idle", + pricingData, + onConfirm, + onContinueLater, + onAbort, + confirmPending = false, + abortPending = false, }: { form: BookingForm; setStep: (step: number) => void; direction: Freight.ScheduleTradeDirection; referenceData?: Freight.BookingReferenceData; + pricingPhase?: "idle" | "generating" | "ready"; + pricingData?: GeneratePriceResponse | null; + onConfirm?: () => void; + onContinueLater?: () => void; + onAbort?: () => void; + confirmPending?: boolean; + abortPending?: boolean; }) { const values = form.watch(); const errors = form.formState.errors; @@ -272,6 +299,75 @@ export function Step8Review({ /> )} /> + + {pricingPhase === "generating" && ( + + + + + Generating price estimate… + + + + )} + + {pricingPhase === "ready" && pricingData && ( + + + Price Estimate + + {pricingData.lineItems.map((item) => ( + + {item.description} + + {item.amount.toLocaleString()} {item.currency} + + + ))} + + + Total + + {pricingData.totalAmount.toLocaleString()} {pricingData.currency} + + + {pricingData.warnings.length > 0 && ( + + {pricingData.warnings.join(", ")} + + )} + + + + + + + )}
); } From 1ca98f32eade377d5cbceb551e346703de6b73b0 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Wed, 17 Jun 2026 06:24:03 +0000 Subject: [PATCH 4/7] style: improved the review ui --- .../src/pages/bookings/EditBookingPage.tsx | 28 +- .../pages/bookings/new-booking-form/schema.ts | 8 +- .../new-booking-form/step8-review.tsx | 463 +++++++++--------- 3 files changed, 243 insertions(+), 256 deletions(-) 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 edd83df15..b81cc455a 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx @@ -1,8 +1,7 @@ -import { useMemo, useRef, type ReactNode } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { api } from "@/services/api"; +import type { CreateBookingPayload } from "@/services/bookings.service"; +import type { Freight } from "@edr/types"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useNavigate, useParams } from "react-router-dom"; import { ActionIcon, Alert, @@ -21,6 +20,7 @@ import { TextInput, Title, } from "@mantine/core"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertCircle, AlertTriangle, @@ -34,12 +34,17 @@ import { Upload, X, } from "lucide-react"; -import type { Freight } from "@edr/types"; -import { api } from "@/services/api"; -import type { CreateBookingPayload } from "@/services/bookings.service"; +import { useMemo, useRef, type ReactNode } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useNavigate, useParams } from "react-router-dom"; +import { + CountChip, + DocRow, + IconSquare, +} from "./BookingDetailPage/components/Documents"; import { - BookingFormInputValues, BOOKING_DOCS_SETTING, + BookingFormInputValues, bookingFormSchema, getRouteDirection, initialBookingFormValues, @@ -48,11 +53,6 @@ import { } from "./new-booking-form/schema"; import { SelectField } from "./new-booking-form/shared"; import { Step5CargoDetails } from "./new-booking-form/steps"; -import { - CountChip, - DocRow, - IconSquare, -} from "./BookingDetailPage/components/Documents"; function yardNameFromBooking( yard: { label?: string; code?: string; name?: string } | undefined | null, @@ -117,8 +117,6 @@ function mapBookingToFormValues( shippingLine: (booking as any).shippingLine?.name ?? "", consolidationEnabled: booking.allowConsolidation ?? false, notes: "", - // Terms were accepted at creation; editing shouldn't re-gate on them. - termsAccepted: true, containers: [], } as BookingFormInputValues; diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts index 3d1a7a0bf..3a59e5715 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts @@ -121,7 +121,6 @@ export const bookingFormSchema = z consolidationEnabled: z.boolean(), documents: z.record(z.string(), z.any()).default({}), notes: z.string(), - termsAccepted: z.boolean(), }) .refine( (data) => @@ -165,10 +164,6 @@ export const bookingFormSchema = z (data) => !(data.cargoType === "container" && data.containers.length === 0), { message: "Add at least one container.", path: ["containers"] }, ) - .refine((data) => data.termsAccepted, { - message: "Accept the freight contract terms to submit.", - path: ["termsAccepted"], - }) .superRefine((data, ctx) => { if (data.cargoType === "bulk") { if (!data.cargoTypePath[0]) { @@ -237,7 +232,6 @@ export const initialBookingFormValues: DeepPartial = { consolidationEnabled: false, documents: {}, notes: "", - termsAccepted: false, }; export const stepFields: Record>> = { @@ -265,7 +259,7 @@ export const stepFields: Record>> = { ], 5: ["scheduledDate", "trainScheduleId"], 6: ["documents"], - 7: ["notes", "termsAccepted"], + 7: ["notes"], }; export interface ContainerConfig { 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 bbfd1cc34..278698f83 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 @@ -3,15 +3,15 @@ import { Box, Button, Card, - Checkbox, Divider, Group, Loader, SimpleGrid, + Stack, Text, Textarea, } from "@mantine/core"; -import { Check, Send, XCircle } from "lucide-react"; +import { Check, Send, XCircle, FileText, Route, Package, Truck } from "lucide-react"; import { BookingFormInputValues, BOOKING_DOCS_SETTING, @@ -54,12 +54,11 @@ export function Step8Review({ abortPending?: boolean; }) { const values = form.watch(); - const errors = form.formState.errors; const serviceType = referenceData?.service.find( (s) => s.id === values.serviceTypeId, ); - function Row({ + function CompactRow({ label, value, target, @@ -69,19 +68,19 @@ export function Step8Review({ target: number; }) { return ( -
-
- +
+
+ {label} - + {value || "—"}
@@ -89,6 +88,28 @@ export function Step8Review({ ); } + function CompactCard({ + icon: Icon, + title, + children, + }: { + icon: React.ReactNode; + title: string; + children: React.ReactNode; + }) { + return ( + + + {Icon} + + {title} + + + {children} + + ); + } + const containerSummary = values.cargoType === "container" && values.containers.length > 0 ? values.containers @@ -122,186 +143,24 @@ export function Step8Review({ return child ? `${group.name} — ${child.name}` : group.name; })(); - function ReviewCard({ - title, - children, - }: { - title: string; - children: React.ReactNode; - }) { - return ( - - - - {title} - - - - {children} - - - ); - } + const originYardName = referenceData?.yard.find( + (y) => y.id === values.originYard, + )?.name ?? values.originYard; + + const destinationYardName = referenceData?.yard.find( + (y) => y.id === values.destinationYard, + )?.name ?? values.destinationYard; return ( -
+ - - - - - - - - - - - - - - - - - - - - - - - - 0 ? `${totalVgm.toFixed(1)} tons` : ""} - target={4} - /> - - - - 0 - ? `${docsAttached} of ${docsTotal} attached` - : "None — upload later from the booking page" - } - target={5} - /> - - - - ( -