From f3284a13d099f7252c50d2ffc3a444803f87f96f Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Tue, 16 Jun 2026 17:25:29 +0300 Subject: [PATCH] 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 && ( + + )} + )}