diff --git a/apps/edr-freight-web/portal/package.json b/apps/edr-freight-web/portal/package.json index 81a34f736..9afc896f7 100644 --- a/apps/edr-freight-web/portal/package.json +++ b/apps/edr-freight-web/portal/package.json @@ -14,6 +14,7 @@ "dependencies": { "@edr/types": "workspace:*", "@edr/ui-common": "workspace:*", + "@hookform/resolvers": "^5.4.0", "@tanstack/react-query": "^5.59.0", "@tria-plc/iamui-common": "1.1.1", "axios": "^1.7.7", @@ -23,9 +24,11 @@ "radix-ui": "^1.4.3", "react": "19.2.6", "react-dom": "19.2.6", + "react-hook-form": "^7.76.0", "react-router-dom": "^6.27.0", "recharts": "^3.8.1", "tailwind-merge": "^3.6.0", + "zod": "^4.4.3", "zustand": "^5.0.0" }, "devDependencies": { diff --git a/apps/edr-freight-web/portal/src/components/dynamic-select/DynamicSelect.tsx b/apps/edr-freight-web/portal/src/components/dynamic-select/DynamicSelect.tsx new file mode 100644 index 000000000..2003457f0 --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/dynamic-select/DynamicSelect.tsx @@ -0,0 +1,99 @@ +import { useQuery } from "@tanstack/react-query"; +import { cn } from "@/lib/utils"; +import { api } from "@/services/api"; +import { Loader2, AlertCircle } from "lucide-react"; +import * as React from "react"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@edr/ui-common"; + +export interface DynamicSelectProps { + code: string; + placeholder?: string; + value?: string; + onValueChange?: (value: string) => void; + disabled?: boolean; + className?: string; +} + +export function DynamicSelect({ + code, + placeholder, + value, + onValueChange, + disabled, + className, +}: DynamicSelectProps) { + const { data, isLoading, isError } = useQuery( + api.dropdownSettings.getByCode.queryOptions({ input: { code } }), + ); + + if (isLoading) { + return ( +
+ + Loading... +
+ ); + } + + if (isError || !data) { + return ( +
+ + Failed to load options +
+ ); + } + + const options = [...data.children].sort((a, b) => a.order - b.order); + + return ( + + ); +} diff --git a/apps/edr-freight-web/portal/src/components/dynamic-select/index.ts b/apps/edr-freight-web/portal/src/components/dynamic-select/index.ts new file mode 100644 index 000000000..4e6c58b62 --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/dynamic-select/index.ts @@ -0,0 +1,2 @@ +export { DynamicSelect } from "./DynamicSelect"; +export type { DynamicSelectProps } from "./DynamicSelect"; diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx index cd1bd1e99..7eb038196 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx @@ -14,14 +14,9 @@ import { } from "lucide-react"; import Breadcrumbs from "@/components/Breadcrumbs"; -import NewBookingPage from "./NewBookingPage"; import DeleteBookingDialog from "./DeleteBookingDialog"; import { getBookingById, type BookingStatus } from "./bookings.mock"; -import { - Button, - Card, - CardContent, -} from "@edr/ui-common"; +import { Button, Card } from "@edr/ui-common"; export default function BookingDetailPage() { const { id } = useParams<{ id: string }>(); @@ -89,26 +84,7 @@ export default function BookingDetailPage() {
- - - + - {booking.transportMode === "Multimodal" && booking.legs && booking.legs.length > 0 ? ( + {booking.transportMode === "Multimodal" && + booking.legs && + booking.legs.length > 0 ? (
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingsPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingsPage.tsx index e755c174b..ad7a6cf92 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingsPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingsPage.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { useNavigate } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { ArrowRight, Clock, @@ -15,7 +15,6 @@ import { } from "lucide-react"; import Breadcrumbs from "@/components/Breadcrumbs"; -import NewBookingPage from "./NewBookingPage"; import DeleteBookingDialog from "./DeleteBookingDialog"; import { bookings, type BookingStatus } from "./bookings.mock"; import { @@ -125,29 +124,6 @@ export default function BookingsPage() { View - - e.preventDefault()}> - - Edit - -
- + - +
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 6d127eb70..e23b44cb2 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -1,1301 +1,124 @@ -import { Fragment, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; -import { - AlertTriangle, - Check, - CheckCircle2, - ChevronLeft, - ChevronRight, - FileText, - Flame, - Info, - Loader2, - MapPin, - Package, - RefreshCw, - Snowflake, - Train, - Weight, - XCircle, -} from "lucide-react"; -import { - Badge, - Button, - Card, - CardContent, - CardHeader, - CardTitle, - Input, - Label, - SmartFileInput, - Textarea, -} from "@edr/ui-common"; +import { CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react"; +import { Button } from "@edr/ui-common"; import Breadcrumbs from "@/components/Breadcrumbs"; - -// ── Mock Constants ───────────────────────────────────────────────────────────── - -const STATIONS = [ - "Addis Ababa", "Adama", "Mojo", "Awash", "Mieso", - "Dire Dawa", "Aysha", "Ali Sabieh", "Holhol", "Djibouti City", -]; - -const ETHIOPIA_STATIONS = new Set([ - "Addis Ababa", "Adama", "Mojo", "Awash", "Mieso", "Dire Dawa", -]); - -const BULK_COMMODITIES = [ - "Coffee", "Beans", "Fertilizer", "Sugar", "Oil", "Livestock", "Steel", "Others", -]; - -const BREAK_BULK_TYPES = ["Machinery", "Ro-Ro", "Others"]; - -const MOCK_VALID_CONTRACTS = [ - "EDR-2024-10001", "EDR-2024-10002", "EDR-2023-88123", "EDR-2022-55442", -]; - -// US-04 compliance documents (TIN, Business License, Registration, National ID, optional PoA) -const BOOKING_DOCS_SETTING = { - id: "booking-compliance", - createdAt: "", - updatedAt: "", - deletedAt: null, - code: "booking_compliance_docs", - label: "Compliance Documents", - description: - "Upload your company's legal credentials. All mandatory documents must be submitted before the contract request can be reviewed by EDR Line Staff.", - entity: "booking" as const, - fields: [ - { - id: "f1", createdAt: "", updatedAt: "", deletedAt: null, - settingId: "booking-compliance", - fileKey: "tin_certificate", - fileLabel: "TIN Certificate", - helpText: "Tax Identification Number certificate issued by ERCA (10-digit TIN).", - isRequired: true, - isMultiple: false, - maxFiles: 1, - allowedExtensions: ["pdf", "jpg", "jpeg", "png"], - maxSizeMb: 5, - order: 1, - }, - { - id: "f2", createdAt: "", updatedAt: "", deletedAt: null, - settingId: "booking-compliance", - fileKey: "business_license", - fileLabel: "Business / Investment License", - helpText: "Current business or investment license issued by the relevant government authority.", - isRequired: true, - isMultiple: false, - maxFiles: 1, - allowedExtensions: ["pdf", "jpg", "jpeg", "png"], - maxSizeMb: 5, - order: 2, - }, - { - id: "f3", createdAt: "", updatedAt: "", deletedAt: null, - settingId: "booking-compliance", - fileKey: "business_registration", - fileLabel: "Business Registration Certificate", - helpText: "Certificate of registration from the relevant authority.", - isRequired: true, - isMultiple: false, - maxFiles: 1, - allowedExtensions: ["pdf", "jpg", "jpeg", "png"], - maxSizeMb: 5, - order: 3, - }, - { - id: "f4", createdAt: "", updatedAt: "", deletedAt: null, - settingId: "booking-compliance", - fileKey: "national_id", - fileLabel: "National ID / Passport", - helpText: "Valid government-issued ID or passport of the authorized signatory.", - isRequired: true, - isMultiple: false, - maxFiles: 1, - allowedExtensions: ["pdf", "jpg", "jpeg", "png"], - maxSizeMb: 5, - order: 4, - }, - { - id: "f5", createdAt: "", updatedAt: "", deletedAt: null, - settingId: "booking-compliance", - fileKey: "power_of_attorney", - fileLabel: "Power of Attorney (PoA)", - helpText: "Required only if a representative is signing on behalf of the company.", - isRequired: false, - isMultiple: false, - maxFiles: 1, - allowedExtensions: ["pdf", "jpg", "jpeg", "png"], - maxSizeMb: 5, - order: 5, - }, - ], -}; - -const REQUIRED_DOC_KEYS = [ - "tin_certificate", - "business_license", - "business_registration", - "national_id", -]; - -const STEPS = [ - { id: 1, label: "Contract Type", short: "Contract" }, - { id: 2, label: "Service Type", short: "Service" }, - { id: 3, label: "First & Last Mile", short: "Mile" }, - { id: 4, label: "Route & Cargo", short: "Cargo" }, - { id: 5, label: "Container Config", short: "Container" }, - { id: 6, label: "Wagon Allocation", short: "Wagons" }, - { id: 7, label: "Documents", short: "Docs" }, - { id: 8, label: "Review & Submit", short: "Submit" }, -] as const; - -// ── Types ────────────────────────────────────────────────────────────────────── - -type ContractTypeVal = "new" | "renewal" | ""; -type ServiceTypeVal = "rail" | "rail_forwarding" | ""; -type FreightTypeVal = "bulk" | "break_bulk" | ""; -type ContainerTypeVal = "20ft" | "40ft" | ""; -type EquipmentReturn = "with_return" | "without_return"; - -interface FormData { - contractType: ContractTypeVal; - previousContractRef: string; - draftContractId: string; - serviceType: ServiceTypeVal; - firstMileEnabled: boolean; - pickUpAddress: string; - lastMileEnabled: boolean; - deliveryAddress: string; - equipmentReturn: EquipmentReturn; - originYard: string; - destinationYard: string; - cargoWeight: string; - freightType: FreightTypeVal; - bulkCommodity: string; - bulkCommodityOther: string; - breakBulkType: string; - breakBulkTypeOther: string; - isHazardous: boolean; - isRefrigerated: boolean; - containerType: ContainerTypeVal; - quantity: string; - vgm: string; - consolidationEnabled: boolean; - documents: Record; - notes: string; - termsAccepted: boolean; -} - -const INITIAL_DATA: FormData = { - contractType: "", - previousContractRef: "", - draftContractId: "", - serviceType: "", - firstMileEnabled: false, - pickUpAddress: "", - lastMileEnabled: false, - deliveryAddress: "", - equipmentReturn: "with_return", - originYard: "", - destinationYard: "", - cargoWeight: "", - freightType: "", - bulkCommodity: "", - bulkCommodityOther: "", - breakBulkType: "", - breakBulkTypeOther: "", - isHazardous: false, - isRefrigerated: false, - containerType: "", - quantity: "1", - vgm: "", - consolidationEnabled: false, - documents: {}, - notes: "", - termsAccepted: false, -}; - -// ── Helpers ──────────────────────────────────────────────────────────────────── - -function genContractId(): string { - const yr = new Date().getFullYear(); - const n = Math.floor(10000 + Math.random() * 90000); - return `EDR-DRAFT-${yr}-${n}`; -} - -type RouteDirection = "import" | "export" | "domestic" | null; - -function getRouteDirection(origin: string, dest: string): RouteDirection { - if (!origin || !dest) return null; - const oEth = ETHIOPIA_STATIONS.has(origin); - const dEth = ETHIOPIA_STATIONS.has(dest); - if (oEth && !dEth) return "export"; - if (!oEth && dEth) return "import"; - if (oEth && dEth) return "domestic"; - return null; -} - -interface WagonCalcResult { - totalWagons: number; - hasOddUnit: boolean; - sharedWagons: number; -} - -function calcWagons(type: ContainerTypeVal, qty: number): WagonCalcResult { - if (type === "40ft") return { totalWagons: qty, hasOddUnit: false, sharedWagons: qty }; - const pairs = Math.floor(qty / 2); - const odd = qty % 2; - return { totalWagons: pairs + odd, hasOddUnit: odd > 0, sharedWagons: pairs }; -} - -// ── Shared primitives ────────────────────────────────────────────────────────── - -const selectCls = - "w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground shadow-xs outline-none transition hover:border-ring/50 focus:border-primary/50 focus:ring-2 focus:ring-primary/20"; - -function NativeSelect({ - value, onChange, options, placeholder, -}: { - value: string; - onChange: (v: string) => void; - options: { value: string; label: string }[]; - placeholder?: string; -}) { - return ( - - ); -} - -function OptionCard({ - selected, onClick, disabled, children, -}: { - selected: boolean; - onClick?: () => void; - disabled?: boolean; - children: React.ReactNode; -}) { - return ( - - ); -} - -function Toggle({ enabled, onChange }: { enabled: boolean; onChange: (v: boolean) => void }) { - return ( - - ); -} - -function AlertBox({ tone, children }: { - tone: "warning" | "error" | "success" | "info"; - children: React.ReactNode; -}) { - const s = { - 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 icons = { - warning: , - error: , - success: , - info: , - }; - return ( -
- {icons[tone]} -
{children}
-
- ); -} - -function Field({ label, children }: { label: string; children: React.ReactNode }) { - return ( -
- - {children} -
- ); -} - -function Divider() { - return
; -} - -function StepLabel({ children }: { children: React.ReactNode }) { - return ( -

- {children} -

- ); -} - -// ── Step 1: Contract Type ────────────────────────────────────────────────────── - -function Step1({ - d, set, renewalValid, renewalValidating, onValidate, -}: { - d: FormData; - set: (k: K, v: FormData[K]) => void; - renewalValid: boolean | null; - renewalValidating: boolean; - onValidate: () => void; -}) { - return ( -
-
-

Contract Type

-

- New contract or renewal of an existing one. -

-
- -
- { - set("contractType", "new"); - if (!d.draftContractId) set("draftContractId", genContractId()); - }} - > -
- -
-

New Contract

-

- Blank contract form. A draft ID is auto-generated. -

- {d.contractType === "new" && d.draftContractId && ( -

- {d.draftContractId} -

- )} -
- - set("contractType", "renewal")} - > -
- -
-

Contract Renewal

-

- Enter a previous reference to auto-populate historical parameters. -

-
-
- - {d.contractType === "renewal" && ( -
- -
- set("previousContractRef", e.target.value)} - className="font-mono" - /> - -
-
- {renewalValid === true && ( - - Contract found. Company details, route, and wagon preferences will - be pre-filled. - - )} - {renewalValid === false && ( - - Contract Reference Number not found or unauthorized. Try{" "} - EDR-2024-10001. - - )} -
- )} -
- ); -} - -// ── Step 2: Service Type ─────────────────────────────────────────────────────── - -function Step2({ - d, set, -}: { - d: FormData; - set: (k: K, v: FormData[K]) => void; -}) { - return ( -
-
-

Service Type

-

- Select the service combination you require. -

-
- -
- set("serviceType", "rail")}> -
- -
-

Rail Transport Only

-

- Rail transport along the EDR corridor, with optional first/last mile trucking. -

- - Option A - -
- - set("serviceType", "rail_forwarding")} - > -
- -
-

Rail Transport & Freight Forwarding

-

- Rail transport plus documentation, customs liaison, and a dedicated coordinator. -

- - Option B - -
-
- -

- Customs and Clearance Service cannot be selected independently — it must be bundled with - a Rail Transport service. -

-
- ); -} - -// ── Step 3: First & Last Mile ────────────────────────────────────────────────── - -function Step3({ - d, set, -}: { - d: FormData; - set: (k: K, v: FormData[K]) => void; -}) { - return ( -
-
-

First & Last Mile

-

- Configure trucking and container return options. -

-
- -
- {/* First Mile */} -
-
-
-

First Mile – Pick-up

-

- Truck pick-up from your premises to the origin rail yard. -

-
- set("firstMileEnabled", v)} /> -
- {d.firstMileEnabled && ( -
- set("pickUpAddress", e.target.value)} - /> -
- )} -
- - {/* Last Mile */} -
-
-
-

Last Mile – Delivery

-

- Truck delivery from the destination rail yard to the final address. -

-
- set("lastMileEnabled", v)} /> -
- {d.lastMileEnabled && ( -
- set("deliveryAddress", e.target.value)} - /> -
- )} -
- - {/* Equipment Return */} -
-

Equipment Return

-

- Declare whether the container asset will be returned after unloading. -

-
- set("equipmentReturn", "with_return")} - > -

With Return

-

- Container returned to EDR after unloading. -

-
- set("equipmentReturn", "without_return")} - > -

Without Return

-

- Container retained by the customer after delivery. -

-
-
-
-
-
- ); -} - -// ── Step 4: Route & Cargo ────────────────────────────────────────────────────── - -function Step4({ - d, set, -}: { - d: FormData; - set: (k: K, v: FormData[K]) => void; -}) { - const direction = getRouteDirection(d.originYard, d.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", - }; - const directionLabel: Record = { - export: "Export workflow (Ethiopia → Djibouti)", - import: "Import workflow (Djibouti → Ethiopia)", - domestic: "Domestic corridor", - }; - - return ( -
-
-

Route & Cargo

-

- Define the route, weight, and cargo classification. -

-
- - {/* Route */} -
- Route -
- - set("originYard", v)} - options={STATIONS.filter((s) => s !== d.destinationYard).map((s) => ({ - value: s, label: s, - }))} - placeholder="Select origin..." - /> - - - set("destinationYard", v)} - options={STATIONS.filter((s) => s !== d.originYard).map((s) => ({ - value: s, label: s, - }))} - placeholder="Select destination..." - /> - -
- {direction && ( -
- - {directionLabel[direction]} -
- )} -
- - - - {/* Weight */} -
- Weight - -
- - set("cargoWeight", e.target.value)} - className="pl-9" - min="0" - step="0.01" - /> -
-
-
- - - - {/* Freight Classification */} -
- Freight Type * -
- set("freightType", "bulk")} - > -

Bulk

-

- Coffee, fertilizer, grain, ore, etc. -

-
- set("freightType", "break_bulk")} - > -

Break-Bulk

-

- Machinery, vehicles, project cargo, etc. -

-
-
- - {d.freightType === "bulk" && ( -
- { set("bulkCommodity", v); if (v !== "Others") set("bulkCommodityOther", ""); }} - options={BULK_COMMODITIES.map((c) => ({ value: c, label: c }))} - placeholder="Select commodity *" - /> - {d.bulkCommodity === "Others" && ( - set("bulkCommodityOther", e.target.value)} - /> - )} -
- )} - - {d.freightType === "break_bulk" && ( -
- { set("breakBulkType", v); if (v !== "Others") set("breakBulkTypeOther", ""); }} - options={BREAK_BULK_TYPES.map((c) => ({ value: c, label: c }))} - placeholder="Select type *" - /> - {d.breakBulkType === "Others" && ( - set("breakBulkTypeOther", e.target.value)} - /> - )} -
- )} -
- - - - {/* Modifiers */} -
-
-
- -
-

Hazardous Material

-

- Applies a Hazard Surcharge to the final bill. -

-
-
- set("isHazardous", v)} /> -
-
-
- -
-

Refrigerated Cargo

-

- Temperature-controlled transport — applies a Refrigerator Surcharge. -

-
-
- set("isRefrigerated", v)} /> -
-
-
- ); -} - -// ── Step 5: Container Configuration ─────────────────────────────────────────── - -function Step5({ - d, set, direction, -}: { - d: FormData; - set: (k: K, v: FormData[K]) => void; - direction: RouteDirection; -}) { - const qty = parseInt(d.quantity) || 1; - const vgm = parseFloat(d.vgm) || 0; - - let overweightAlert: string | null = null; - if (d.containerType === "20ft" && vgm > 0) { - const limit = direction === "export" ? 25 : 20; - if (vgm > limit) - overweightAlert = `VGM ${vgm}t exceeds the ${limit}t ${direction ?? "standard"} limit for a 20ft container. An Overweight Surcharge will apply.`; - } - if (d.containerType === "40ft" && vgm > 32.5) - overweightAlert = `VGM ${vgm}t exceeds the 32.5t global limit for a 40ft container. An Overweight Surcharge will apply.`; - - return ( -
-
-

Container Configuration

-

- Select container type, quantity, and Verified Gross Mass per container. -

-
- -
- {([ - { - val: "20ft" as const, - label: "20ft Container (TEU)", - limit: direction === "export" ? "Max 25t per container" : "Max 20t per container", - }, - { - val: "40ft" as const, - label: "40ft Container (FEU)", - limit: "Max 32.5t per container", - }, - ]).map((ct) => ( - set("containerType", ct.val)}> -
- -

{ct.label}

-
-

{ct.limit}

-
- ))} -
- - {direction && ( -

- - Route detected as {direction} workflow -

- )} - - - -
- -
- - set("quantity", e.target.value)} - className="text-center" - min="1" - /> - -
-
- - set("vgm", e.target.value)} - min="0" - step="0.1" - /> - -
- - {overweightAlert && Overweight Alert: {overweightAlert}} -
- ); -} - -// ── Step 6: Wagon Allocation ─────────────────────────────────────────────────── - -function Step6({ - d, set, wagons, -}: { - d: FormData; - set: (k: K, v: FormData[K]) => void; - wagons: WagonCalcResult | null; -}) { - const qty = parseInt(d.quantity) || 1; - - return ( -
-
-

Wagon Allocation

-

- System-calculated wagon requirements based on your container profile. -

-
- - {!wagons ? ( - - Complete the container configuration in the previous step to see wagon allocation. - - ) : ( - <> -
-
-

{wagons.totalWagons}

-

Wagons Required

-
-
-

{qty}

-

{d.containerType} Containers

-
-
-

{wagons.sharedWagons}

-

Shared Slots

-
-
- -
- Wagon Layout -
- {Array.from({ length: wagons.totalWagons }, (_, i) => { - const isOdd = wagons.hasOddUnit && i === wagons.totalWagons - 1; - return ( -
- {isOdd - ? `1 × ${d.containerType} (½)` - : d.containerType === "20ft" ? "2 × 20ft" : "1 × 40ft"} -
- ); - })} -
-
- -

- Formula:{" "} - {d.containerType === "40ft" - ? "1 × 40ft = 1 Rail Wagon" - : `CEILING(${qty} ÷ 2) = ${wagons.totalWagons} Rail Wagon${wagons.totalWagons > 1 ? "s" : ""} — (2 × 20ft = 1 Wagon)`} -

- - {wagons.hasOddUnit && ( - <> - -
-
- -

Consolidation Option (US-07)

-
-

- You have 1 unpaired 20ft container. Opt in to share a wagon slot with another - shipper to optimise costs (2 × 20ft = 1 Wagon), or request a dedicated wagon. -

-
- set("consolidationEnabled", true)} - > -

Allow Consolidation

-

- Share a wagon slot — billing split with co-loader. -

-
- set("consolidationEnabled", false)} - > -

Dedicated Wagon

-

- Exclusive slot — standard single-party billing. -

-
-
-
- - )} - - )} -
- ); -} - -// ── Step 7: Compliance Documents ─────────────────────────────────────────────── - -function Step7Docs({ - d, set, -}: { - d: FormData; - set: (k: K, v: FormData[K]) => void; -}) { - const uploadedRequired = REQUIRED_DOC_KEYS.filter((k) => { - const f = d.documents[k]; - if (!f) return false; - return Array.isArray(f) ? f.length > 0 : true; - }).length; - - return ( -
-
-

Compliance Documents

-

- Upload your company's legal credentials for EDR contract eligibility verification (US-04). -

-
- -
-
- {uploadedRequired}/{REQUIRED_DOC_KEYS.length} -
-

- {uploadedRequired < REQUIRED_DOC_KEYS.length - ? `${REQUIRED_DOC_KEYS.length - uploadedRequired} mandatory document(s) still needed.` - : "All mandatory documents uploaded. Power of Attorney is optional."} -

-
- - set("documents", val)} - /> -
- ); -} - -// ── Step 8: Review & Submit ──────────────────────────────────────────────────── - -function Step8Review({ - d, set, setStep, wagons, direction, -}: { - d: FormData; - set: (k: K, v: FormData[K]) => void; - setStep: (n: number) => void; - wagons: WagonCalcResult | null; - direction: RouteDirection; -}) { - function Row({ label, value, target }: { label: string; value: string; target: number }) { - return ( -
-
-

{label}

-

{value || "—"}

-
- -
- ); - } - - const cargoValue = - d.freightType === "bulk" - ? `Bulk — ${d.bulkCommodity === "Others" ? d.bulkCommodityOther : d.bulkCommodity}` - : d.freightType === "break_bulk" - ? `Break-Bulk — ${d.breakBulkType === "Others" ? d.breakBulkTypeOther : d.breakBulkType}` - : ""; - - const uploadedCount = REQUIRED_DOC_KEYS.filter((k) => { - const f = d.documents[k]; - if (!f) return false; - return Array.isArray(f) ? f.length > 0 : true; - }).length; - - return ( -
-
-

Review & Submit

-

- Confirm your contract request before sending it for EDR staff review. -

-
- -
- - - - Contract & Service - - - - - - - - - - - - - First & Last Mile - - - - - - - - - - - - - Route & Cargo - - - - - - - - - - - - - - - Container & Wagons - - - - - - 1 ? "s" : ""}` : ""} - target={6} - /> - - - -
- -
- -