From e9362abecdebddd35c88286ea427ce9c477de135 Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Fri, 22 May 2026 11:38:22 +0300 Subject: [PATCH] feat(bookings): Rework New Booking flow to support cargo type selection --- .../src/pages/bookings/NewBookingPage.tsx | 876 +++++++++++------- 1 file changed, 563 insertions(+), 313 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 6d127eb70..84a3b9c69 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -27,7 +27,13 @@ import { CardTitle, Input, Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, SmartFileInput, + Switch, Textarea, } from "@edr/ui-common"; import Breadcrumbs from "@/components/Breadcrumbs"; @@ -35,22 +41,45 @@ import Breadcrumbs from "@/components/Breadcrumbs"; // ── Mock Constants ───────────────────────────────────────────────────────────── const STATIONS = [ - "Addis Ababa", "Adama", "Mojo", "Awash", "Mieso", - "Dire Dawa", "Aysha", "Ali Sabieh", "Holhol", "Djibouti City", + "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", + "Addis Ababa", + "Adama", + "Mojo", + "Awash", + "Mieso", + "Dire Dawa", ]); const BULK_COMMODITIES = [ - "Coffee", "Beans", "Fertilizer", "Sugar", "Oil", "Livestock", "Steel", "Others", + "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", + "EDR-2024-10001", + "EDR-2024-10002", + "EDR-2023-88123", + "EDR-2022-55442", ]; // US-04 compliance documents (TIN, Business License, Registration, National ID, optional PoA) @@ -66,11 +95,15 @@ const BOOKING_DOCS_SETTING = { entity: "booking" as const, fields: [ { - id: "f1", createdAt: "", updatedAt: "", deletedAt: null, + 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).", + helpText: + "Tax Identification Number certificate issued by ERCA (10-digit TIN).", isRequired: true, isMultiple: false, maxFiles: 1, @@ -79,11 +112,15 @@ const BOOKING_DOCS_SETTING = { order: 1, }, { - id: "f2", createdAt: "", updatedAt: "", deletedAt: null, + 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.", + helpText: + "Current business or investment license issued by the relevant government authority.", isRequired: true, isMultiple: false, maxFiles: 1, @@ -92,7 +129,10 @@ const BOOKING_DOCS_SETTING = { order: 2, }, { - id: "f3", createdAt: "", updatedAt: "", deletedAt: null, + id: "f3", + createdAt: "", + updatedAt: "", + deletedAt: null, settingId: "booking-compliance", fileKey: "business_registration", fileLabel: "Business Registration Certificate", @@ -105,11 +145,15 @@ const BOOKING_DOCS_SETTING = { order: 3, }, { - id: "f4", createdAt: "", updatedAt: "", deletedAt: null, + 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.", + helpText: + "Valid government-issued ID or passport of the authorized signatory.", isRequired: true, isMultiple: false, maxFiles: 1, @@ -118,11 +162,15 @@ const BOOKING_DOCS_SETTING = { order: 4, }, { - id: "f5", createdAt: "", updatedAt: "", deletedAt: null, + 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.", + helpText: + "Required only if a representative is signing on behalf of the company.", isRequired: false, isMultiple: false, maxFiles: 1, @@ -144,8 +192,8 @@ 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: 4, label: "Route", short: "Route" }, + { id: 5, label: "Cargo Details", short: "Cargo" }, { id: 6, label: "Wagon Allocation", short: "Wagons" }, { id: 7, label: "Documents", short: "Docs" }, { id: 8, label: "Review & Submit", short: "Submit" }, @@ -171,6 +219,7 @@ interface FormData { equipmentReturn: EquipmentReturn; originYard: string; destinationYard: string; + cargoType: "container" | "bulk" | ""; cargoWeight: string; freightType: FreightTypeVal; bulkCommodity: string; @@ -200,6 +249,7 @@ const INITIAL_DATA: FormData = { equipmentReturn: "with_return", originYard: "", destinationYard: "", + cargoType: "", cargoWeight: "", freightType: "", bulkCommodity: "", @@ -244,7 +294,8 @@ interface WagonCalcResult { } function calcWagons(type: ContainerTypeVal, qty: number): WagonCalcResult { - if (type === "40ft") return { totalWagons: qty, hasOddUnit: false, sharedWagons: qty }; + 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 }; @@ -252,29 +303,11 @@ function calcWagons(type: ContainerTypeVal, qty: number): WagonCalcResult { // ── 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, + onClick, + disabled, + children, }: { selected: boolean; onClick?: () => void; @@ -286,13 +319,12 @@ function OptionCard({ type="button" onClick={onClick} disabled={disabled} - className={`relative w-full rounded-xl border-2 p-4 text-left transition ${ - disabled + className={`relative w-full rounded-xl border-2 p-4 text-left transition ${disabled ? "cursor-not-allowed border-border bg-muted opacity-60" : selected - ? "border-primary bg-primary/5" - : "border-border bg-card hover:border-primary/40" - }`} + ? "border-primary bg-primary/5" + : "border-border bg-card hover:border-primary/40" + }`} > {selected && !disabled && ( @@ -304,27 +336,10 @@ function OptionCard({ ); } -function Toggle({ enabled, onChange }: { enabled: boolean; onChange: (v: boolean) => void }) { - return ( - - ); -} - -function AlertBox({ tone, children }: { +function AlertBox({ + tone, + children, +}: { tone: "warning" | "error" | "success" | "info"; children: React.ReactNode; }) { @@ -341,14 +356,22 @@ function AlertBox({ tone, children }: { info: , }; return ( -
+
{icons[tone]}
{children}
); } -function Field({ label, children }: { label: string; children: React.ReactNode }) { +function Field({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) { return (
@@ -372,7 +395,11 @@ function StepLabel({ children }: { children: React.ReactNode }) { // ── Step 1: Contract Type ────────────────────────────────────────────────────── function Step1({ - d, set, renewalValid, renewalValidating, onValidate, + d, + set, + renewalValid, + renewalValidating, + onValidate, }: { d: FormData; set: (k: K, v: FormData[K]) => void; @@ -440,14 +467,18 @@ function Step1({ onClick={onValidate} disabled={!d.previousContractRef || renewalValidating} > - {renewalValidating ? : "Validate"} + {renewalValidating ? ( + + ) : ( + "Validate" + )}
{renewalValid === true && ( - Contract found. Company details, route, and wagon preferences will - be pre-filled. + Contract found. Company details, route, and wagon + preferences will be pre-filled. )} {renewalValid === false && ( @@ -465,7 +496,8 @@ function Step1({ // ── Step 2: Service Type ─────────────────────────────────────────────────────── function Step2({ - d, set, + d, + set, }: { d: FormData; set: (k: K, v: FormData[K]) => void; @@ -480,13 +512,17 @@ function Step2({
- set("serviceType", "rail")}> + set("serviceType", "rail")} + >

Rail Transport Only

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

Option A @@ -502,7 +538,8 @@ function Step2({

Rail Transport & Freight Forwarding

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

Option B @@ -511,8 +548,8 @@ function Step2({

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

); @@ -521,7 +558,8 @@ function Step2({ // ── Step 3: First & Last Mile ────────────────────────────────────────────────── function Step3({ - d, set, + d, + set, }: { d: FormData; set: (k: K, v: FormData[K]) => void; @@ -545,7 +583,10 @@ function Step3({ Truck pick-up from your premises to the origin rail yard.

- set("firstMileEnabled", v)} /> + set("firstMileEnabled", v)} + /> {d.firstMileEnabled && (
@@ -564,10 +605,14 @@ function Step3({

Last Mile – Delivery

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

- set("lastMileEnabled", v)} /> + set("lastMileEnabled", v)} + />
{d.lastMileEnabled && (
@@ -584,7 +629,8 @@ function Step3({

Equipment Return

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

(k: K, v: FormData[K]) => void; @@ -635,9 +682,9 @@ function Step4({ return (
-

Route & Cargo

+

Route

- Define the route, weight, and cargo classification. + Select the origin and destination yards.

@@ -646,24 +693,38 @@ function Step4({ Route
- set("originYard", v)} - options={STATIONS.filter((s) => s !== d.destinationYard).map((s) => ({ - value: s, label: s, - }))} - placeholder="Select origin..." - /> + onValueChange={(v) => set("originYard", v)} + > + + + + + {STATIONS.filter((s) => s !== d.destinationYard).map((s) => ( + + {s} + + ))} + + - set("destinationYard", v)} - options={STATIONS.filter((s) => s !== d.originYard).map((s) => ({ - value: s, label: s, - }))} - placeholder="Select destination..." - /> + onValueChange={(v) => set("destinationYard", v)} + > + + + + + {STATIONS.filter((s) => s !== d.originYard).map((s) => ( + + {s} + + ))} + +
{direction && ( @@ -678,90 +739,6 @@ function Step4({ - {/* 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 */}
@@ -774,7 +751,10 @@ function Step4({

- set("isHazardous", v)} /> + set("isHazardous", v)} + />
@@ -782,11 +762,15 @@ function Step4({

Refrigerated Cargo

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

- set("isRefrigerated", v)} /> + set("isRefrigerated", v)} + />
@@ -796,7 +780,9 @@ function Step4({ // ── Step 5: Container Configuration ─────────────────────────────────────────── function Step5({ - d, set, direction, + d, + set, + direction, }: { d: FormData; set: (k: K, v: FormData[K]) => void; @@ -817,79 +803,247 @@ function Step5({ return (
-

Container Configuration

+

Cargo Details

- Select container type, quantity, and Verified Gross Mass per container. + Define your cargo type, weight, and container configuration.

-
- {([ - { - 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)}> -
+ {/* Cargo Type */} +
+ Cargo Type * +
+ set("cargoType", "container")} + > +
-

{ct.label}

-

{ct.limit}

+

Container

+

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

- ))} + set("cargoType", "bulk")} + > +
+ +
+

Bulk

+

+ Bulk commodities or break-bulk cargo. +

+
+
- {direction && ( -

- - Route detected as {direction} workflow -

+ {/* Bulk section */} + {d.cargoType === "bulk" && ( + <> + +
+ 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" && ( +
+ + {d.bulkCommodity === "Others" && ( + set("bulkCommodityOther", e.target.value)} + /> + )} +
+ )} + + {d.freightType === "break_bulk" && ( +
+ + {d.breakBulkType === "Others" && ( + set("breakBulkTypeOther", e.target.value)} + /> + )} +
+ )} +
+ + + + {/* Weight */} +
+ Weight + +
+ + set("cargoWeight", e.target.value)} + className="pl-9" + min="0" + step="0.01" + /> +
+
+
+ )} - + {/* Container section */} + {d.cargoType === "container" && ( + <> + +
+ Container Configuration +
+ {[ + { + 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}

+
+ ))} +
-
- -
- - set("quantity", e.target.value)} - className="text-center" - min="1" - /> - + {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" + /> + +
-
- - set("vgm", e.target.value)} - min="0" - step="0.1" - /> - -
- {overweightAlert && Overweight Alert: {overweightAlert}} + {overweightAlert && ( + + Overweight Alert: {overweightAlert} + + )} + + )}
); } @@ -897,7 +1051,9 @@ function Step5({ // ── Step 6: Wagon Allocation ─────────────────────────────────────────────────── function Step6({ - d, set, wagons, + d, + set, + wagons, }: { d: FormData; set: (k: K, v: FormData[K]) => void; @@ -916,18 +1072,25 @@ function Step6({ {!wagons ? ( - Complete the container configuration in the previous step to see wagon allocation. + Complete the container configuration in the previous step to see wagon + allocation. ) : ( <>
-

{wagons.totalWagons}

-

Wagons Required

+

+ {wagons.totalWagons} +

+

+ Wagons Required +

{qty}

-

{d.containerType} Containers

+

+ {d.containerType} Containers +

{wagons.sharedWagons}

@@ -943,15 +1106,16 @@ function Step6({ return (
{isOdd ? `1 × ${d.containerType} (½)` - : d.containerType === "20ft" ? "2 × 20ft" : "1 × 40ft"} + : d.containerType === "20ft" + ? "2 × 20ft" + : "1 × 40ft"}
); })} @@ -971,11 +1135,14 @@ function Step6({
-

Consolidation Option (US-07)

+

+ 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. + 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.

(k: K, v: FormData[K]) => void; @@ -1023,19 +1191,21 @@ function Step7Docs({ return (
-

Compliance Documents

+

+ Compliance Documents +

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

{uploadedRequired}/{REQUIRED_DOC_KEYS.length}
@@ -1058,7 +1228,11 @@ function Step7Docs({ // ── Step 8: Review & Submit ──────────────────────────────────────────────────── function Step8Review({ - d, set, setStep, wagons, direction, + d, + set, + setStep, + wagons, + direction, }: { d: FormData; set: (k: K, v: FormData[K]) => void; @@ -1066,7 +1240,15 @@ function Step8Review({ wagons: WagonCalcResult | null; direction: RouteDirection; }) { - function Row({ label, value, target }: { label: string; value: string; target: number }) { + function Row({ + label, + value, + target, + }: { + label: string; + value: string; + target: number; + }) { return (
@@ -1088,8 +1270,8 @@ function Step8Review({ d.freightType === "bulk" ? `Bulk — ${d.bulkCommodity === "Others" ? d.bulkCommodityOther : d.bulkCommodity}` : d.freightType === "break_bulk" - ? `Break-Bulk — ${d.breakBulkType === "Others" ? d.breakBulkTypeOther : d.breakBulkType}` - : ""; + ? `Break-Bulk — ${d.breakBulkType === "Others" ? d.breakBulkTypeOther : d.breakBulkType}` + : ""; const uploadedCount = REQUIRED_DOC_KEYS.filter((k) => { const f = d.documents[k]; @@ -1114,11 +1296,25 @@ function Step8Review({ - - + + @@ -1143,7 +1339,11 @@ function Step8Review({ /> @@ -1156,13 +1356,36 @@ function Step8Review({ - - - - + + + + @@ -1175,11 +1398,23 @@ function Step8Review({ - - + + 1 ? "s" : ""}` : ""} + value={ + wagons + ? `${wagons.totalWagons} wagon${wagons.totalWagons > 1 ? "s" : ""}` + : "" + } target={6} /> set("termsAccepted", !d.termsAccepted)} - className={`mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded border-2 transition ${ - d.termsAccepted ? "border-primary bg-primary" : "border-input" - }`} + className={`mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded border-2 transition ${d.termsAccepted ? "border-primary bg-primary" : "border-input" + }`} > - {d.termsAccepted && } + {d.termsAccepted && ( + + )} I confirm the information is accurate and agree to EDR's{" "} - freight contract terms and conditions. + + freight contract terms and conditions + + .
@@ -1250,7 +1489,9 @@ export default function NewBookingPage() { setRenewalValid(null); setTimeout(() => { setRenewalValidating(false); - setRenewalValid(MOCK_VALID_CONTRACTS.includes(data.previousContractRef.toUpperCase())); + setRenewalValid( + MOCK_VALID_CONTRACTS.includes(data.previousContractRef.toUpperCase()), + ); }, 1200); } @@ -1267,18 +1508,25 @@ export default function NewBookingPage() { if (data.lastMileEnabled && !data.deliveryAddress) return false; return true; case 4: - if (!data.originYard || !data.destinationYard || !data.cargoWeight || !data.freightType) - return false; - if (data.freightType === "bulk") { - if (!data.bulkCommodity) return false; - if (data.bulkCommodity === "Others" && !data.bulkCommodityOther) return false; - } - if (data.freightType === "break_bulk") { - if (!data.breakBulkType) return false; - if (data.breakBulkType === "Others" && !data.breakBulkTypeOther) return false; - } + if (!data.originYard || !data.destinationYard) return false; return true; case 5: + if (!data.cargoType) return false; + if (data.cargoType === "bulk") { + if (!data.freightType) return false; + if (data.freightType === "bulk") { + if (!data.bulkCommodity) return false; + if (data.bulkCommodity === "Others" && !data.bulkCommodityOther) + return false; + } + if (data.freightType === "break_bulk") { + if (!data.breakBulkType) return false; + if (data.breakBulkType === "Others" && !data.breakBulkTypeOther) + return false; + } + if (!data.cargoWeight) return false; + return true; + } return !!(data.containerType && data.quantity && data.vgm); case 6: return true; @@ -1309,8 +1557,8 @@ export default function NewBookingPage() {

Contract Submitted

- Your request is queued for review by EDR Line Staff. You will be notified once - approved. + Your request is queued for review by EDR Line Staff. You will be + notified once approved.

{data.draftContractId || data.previousContractRef} @@ -1321,41 +1569,41 @@ export default function NewBookingPage() { } return ( -

+
{/* Sticky step indicator */}
{STEPS.map((s, idx) => (
s.id + className={`flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold transition-colors ${step > s.id ? "bg-primary text-primary-foreground" : step === s.id - ? "border-2 border-primary text-primary" - : "bg-muted text-muted-foreground" - }`} + ? "border-2 border-primary text-primary" + : "bg-muted text-muted-foreground" + }`} > {step > s.id ? : s.id}
= s.id ? "text-foreground" : "text-muted-foreground" - }`} + className={`hidden text-[10px] font-medium lg:block ${step >= s.id ? "text-foreground" : "text-muted-foreground" + }`} > {s.short}
{idx < STEPS.length - 1 && (
s.id ? "bg-primary" : "bg-border" - }`} + className={`mx-1 h-0.5 flex-1 rounded-full transition-colors ${step > s.id ? "bg-primary" : "bg-border" + }`} /> )} @@ -1406,9 +1654,11 @@ export default function NewBookingPage() { Back
- {step} / {STEPS.length} {step < STEPS.length ? ( -