From 6757a3b4070069a193e17e1f6fa5f9084694bc5d Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 12:24:37 -0700 Subject: [PATCH 001/119] style(edit): simplify navbar as to match the new figma design --- .gitignore | 1 + .../layout/navbar/MobileBottomBar.tsx | 31 +--------- .../edit/components/layout/navbar/Navbar.tsx | 56 +++---------------- app/ui/src/app/edit/formStyles.v2.ts | 3 + app/ui/src/app/edit/page.tsx | 23 +------- 5 files changed, 17 insertions(+), 97 deletions(-) diff --git a/.gitignore b/.gitignore index 10e82939..1a6a98ca 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,4 @@ services/vroom/data/ # Claude Claude.md +.claude/ diff --git a/app/ui/src/app/edit/components/layout/navbar/MobileBottomBar.tsx b/app/ui/src/app/edit/components/layout/navbar/MobileBottomBar.tsx index fbeb4320..a51e18f2 100644 --- a/app/ui/src/app/edit/components/layout/navbar/MobileBottomBar.tsx +++ b/app/ui/src/app/edit/components/layout/navbar/MobileBottomBar.tsx @@ -1,41 +1,19 @@ import { MOBILE_BOTTOM_BAR_ROOT, MOBILE_BOTTOM_BAR_INNER, - MOBILE_BOTTOM_BAR_OPTIMIZE_BTN, - MOBILE_BOTTOM_BAR_OPTIMIZE_LABEL, MOBILE_BOTTOM_BAR_ACTIONS_ROW, MOBILE_BOTTOM_BAR_SECONDARY_BTN, MOBILE_BOTTOM_BAR_SECONDARY_LABEL, } from "@/app/edit/formStyles.v2"; -import styles from "@/app/edit/edit.module.css"; type Props = { - onOptimize: () => void; onSave: () => void; - onExport: () => void; - isOptimizing?: boolean; }; -export default function MobileBottomBar({ - onOptimize, - onSave, - onExport, - isOptimizing, -}: Props) { +export default function MobileBottomBar({ onSave }: Props) { return (
-
-
diff --git a/app/ui/src/app/edit/components/layout/navbar/Navbar.tsx b/app/ui/src/app/edit/components/layout/navbar/Navbar.tsx index f58c8cbb..396659ac 100644 --- a/app/ui/src/app/edit/components/layout/navbar/Navbar.tsx +++ b/app/ui/src/app/edit/components/layout/navbar/Navbar.tsx @@ -1,60 +1,22 @@ "use client"; import { - NAVBAR_V2_ACTIONS, - NAVBAR_V2_BTN_FILLED, - NAVBAR_V2_BTN_OUTLINE, + NAVBAR_V2_BTN_SAVE, NAVBAR_V2_LOGO, NAVBAR_V2_ROOT, } from "@/app/edit/formStyles.v2"; -import styles from "@/app/edit/edit.module.css"; -import ErrorPopup from "@/app/edit/components/shared/ErrorPopup"; type NavbarProps = { - onImportSession: () => void; - onExportSession: () => void; - onOptimize: () => void; - isOptimizing: boolean; - error: string | null; - onClearError: () => void; + onSave: () => void; }; -export default function Navbar({ - onImportSession, - onExportSession, - onOptimize, - isOptimizing, - error, - onClearError, -}: NavbarProps) { +export default function Navbar({ onSave }: NavbarProps) { return ( - <> - -
- DELIVERY OPTIMIZER -
- - - -
-
- +
+ Delivery Optimizer + +
); } diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index ce396ea6..42596111 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -105,6 +105,9 @@ export const NAVBAR_V2_LOGO = export const NAVBAR_V2_ACTIONS = "flex items-center gap-2"; +export const NAVBAR_V2_BTN_SAVE = + "h-9 px-[16px] rounded-[5px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; + export const NAVBAR_V2_BTN_OUTLINE = "h-9 px-4 rounded-[80px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"; diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index f83155c0..c05e9b63 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -60,7 +60,7 @@ export default function Page() { optimize, isOptimizing, optimizeError, - clearOptimizeError, + clearOptimizeError, needsDepotAddress, dismissDepotAddressPrompt, geocodeFailedAddressIds, @@ -204,26 +204,9 @@ export default function Page() { isOpen={isMobileMenuOpen} onClose={() => setIsMobileMenuOpen(false)} /> - void optimize()} - onSave={handleExportSession} - onExport={handleExportSession} - isOptimizing={isOptimizing} - /> + {}} /> setIsMobileMenuOpen(true)} /> - void optimize()} - isOptimizing={isOptimizing} - error={sessionError ?? optimizeError ?? csvError ?? parseError} - onClearError={() => { - clearSessionError(); - clearOptimizeError(); - clearCsvError(); - closeImportModal(); - }} - /> + {}} />
From 1fe50cbf7de11dd752f7dce06c346bb177756de2 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 12:31:38 -0700 Subject: [PATCH 002/119] fix(edit): update button label in VehicleDetailsOverlay from 'Done' to 'Confirm' for improved clarity --- .../src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx index 20e389ec..c746a5a4 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx @@ -520,7 +520,7 @@ export default function VehicleDetailsOverlay({ onClick={handleSave} className={`${OVERLAY_PRIMARY_BTN} ${styles.primaryBtnOverlay}`} > - Done + Confirm
From 6081d5c98f28db139c7da2f06492e858fa73d652 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 12:43:28 -0700 Subject: [PATCH 003/119] style(edit): change the location of the optimize button --- .../components/vehicle/VehicleSection.tsx | 20 ++++++++++++++++++- app/ui/src/app/edit/formStyles.v2.ts | 5 +++++ app/ui/src/app/edit/page.tsx | 4 ++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx index 98f2f032..2df10d74 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx @@ -5,6 +5,7 @@ */ import { useState } from "react"; +import styles from "@/app/edit/edit.module.css"; import VehicleRow from "@/app/edit/components/vehicle/VehicleRow"; import VehicleEmptyState from "@/app/edit/components/vehicle/VehicleEmptyState"; import VehicleDetailsOverlay from "@/app/edit/components/vehicle/VehicleDetailsOverlay"; @@ -22,6 +23,8 @@ import { VEHICLE_SECTION_ACTIONS, VEHICLE_SECTION_HEADER, VEHICLE_SECTION_HEADING, + VEHICLE_SECTION_HEADING_ROW, + VEHICLE_SECTION_OPTIMIZE_BTN, VEHICLE_SECTION_SUBHEADING, MOBILE_EMPTY_STATE_CONTAINER, VEHICLE_MOBILE_LIST, @@ -66,6 +69,8 @@ type VehicleSectionProps = { activeVehicleIsValid: boolean; geocodeFailedVehicleIds: number[]; outOfRegionVehicleIds: number[]; + onOptimize: () => void; + isOptimizing: boolean; }; export default function VehicleSection({ @@ -79,6 +84,8 @@ export default function VehicleSection({ activeVehicleIsValid, geocodeFailedVehicleIds, outOfRegionVehicleIds, + onOptimize, + isOptimizing, }: VehicleSectionProps) { const [isAddOverlayOpen, setIsAddOverlayOpen] = useState(false); const [editingVehicle, setEditingVehicle] = useState( @@ -110,7 +117,18 @@ export default function VehicleSection({ return (
-

Vehicle details

+
+

Vehicle details

+ +

Manage your delivery fleet

diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 42596111..72280f84 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -211,6 +211,11 @@ export const VEHICLE_SECTION_BTN_GHOST = export const VEHICLE_SECTION_ACTIONS = "flex items-center justify-end gap-2 mb-4"; +export const VEHICLE_SECTION_HEADING_ROW = "flex items-center justify-between"; + +export const VEHICLE_SECTION_OPTIMIZE_BTN = + "h-9 px-[16px] rounded-[4px] bg-[var(--edit-btn-primary)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip cursor-pointer disabled:cursor-not-allowed"; + export const VEHICLE_SECTION_HEADER = "flex flex-col gap-2 mb-4"; export const VEHICLE_SECTION_HEADING = diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index c05e9b63..2d587393 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -10,6 +10,7 @@ import Navbar from "@/app/edit/components/layout/navbar/Navbar"; import MobileNavbar from "@/app/edit/components/layout/navbar/MobileNavbar"; import MobileSidebar from "@/app/edit/components/layout/sidebar/MobileSidebar"; import OptimizingModal from "@/app/edit/components/shared/OptimizingModal"; +import ErrorPopup from "@/app/edit/components/shared/ErrorPopup"; import Sidebar from "@/app/edit/components/layout/sidebar/Sidebar"; import SidebarEditButton from "@/app/edit/components/layout/sidebar/SidebarEditButton"; import SidebarResultsButton from "@/app/edit/components/layout/sidebar/SidebarResultsButton"; @@ -192,6 +193,7 @@ export default function Page() { /> )} + {needsDepotAddress && ( void optimize()} + isOptimizing={isOptimizing} />
Date: Sat, 23 May 2026 12:47:04 -0700 Subject: [PATCH 004/119] style(edit): modify the design of the available/in-use toggle buttons --- .../vehicle/VehicleDetailsOverlay.tsx | 49 +++++----- .../edit/components/vehicle/VehicleRow.tsx | 89 +++++++++++++------ app/ui/src/app/edit/formStyles.v2.ts | 15 +++- 3 files changed, 103 insertions(+), 50 deletions(-) diff --git a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx index c746a5a4..edd8110f 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx @@ -36,12 +36,12 @@ import { OVERLAY_SELECT_ICON, OVERLAY_SELECT_WRAPPER, OVERLAY_SELECT_WRAPPER_ERROR, - OVERLAY_STATUS_BADGE_AVAILABLE, - OVERLAY_STATUS_BADGE_TEXT_AVAILABLE, - OVERLAY_STATUS_BADGE_TEXT_IN_USE, - OVERLAY_STATUS_BADGE_IN_USE, OVERLAY_STATUS_HINT, OVERLAY_STATUS_ROW, + STATUS_TOGGLE_WRAPPER, + STATUS_TOGGLE_BTN_ACTIVE, + STATUS_TOGGLE_BTN_INACTIVE, + STATUS_TOGGLE_TEXT, OVERLAY_SCROLL_BODY, OVERLAY_TIME_SEGMENTS, OVERLAY_TITLE, @@ -383,27 +383,36 @@ export default function VehicleDetailsOverlay({
Status
- + + In use + +

Tap to toggle

diff --git a/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx b/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx index 77624820..2bed40d4 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx @@ -16,11 +16,11 @@ import { VEHICLE_ROW_CELL, VEHICLE_ROW_ACTIONS, VEHICLE_ROW_DESKTOP, - VEHICLE_ROW_STATUS_BADGE_AVAILABLE, - VEHICLE_ROW_STATUS_BADGE_IN_USE, VEHICLE_ROW_STATUS_CELL, - VEHICLE_ROW_STATUS_TEXT_AVAILABLE, - VEHICLE_ROW_STATUS_TEXT_IN_USE, + STATUS_TOGGLE_WRAPPER, + STATUS_TOGGLE_BTN_ACTIVE, + STATUS_TOGGLE_BTN_INACTIVE, + STATUS_TOGGLE_TEXT, VEHICLE_MOBILE_LOCKED_CARD_V2, VEHICLE_MOBILE_LOCKED_HEADER, VEHICLE_MOBILE_LOCKED_INFO, @@ -59,13 +59,6 @@ export default function VehicleRow({ deleteVehicle, onEditVehicle, }: VehicleRowProps) { - const statusBadge = v.available - ? VEHICLE_ROW_STATUS_BADGE_AVAILABLE - : VEHICLE_ROW_STATUS_BADGE_IN_USE; - const statusText = v.available - ? VEHICLE_ROW_STATUS_TEXT_AVAILABLE - : VEHICLE_ROW_STATUS_TEXT_IN_USE; - if (layout === "mobile") { return (
@@ -87,16 +80,36 @@ export default function VehicleRow({
- + + +
{(v.departureTime || "--:--") + " departure time"} @@ -115,16 +128,34 @@ export default function VehicleRow({ {formatCapacity(v)} - + + + {v.departureTime}
diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 72280f84..b8927833 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -155,7 +155,7 @@ export const VEHICLE_EMPTY_STATE_TITLE = EMPTY_STATE_TITLE; export const VEHICLE_EMPTY_STATE_SUBTITLE = EMPTY_STATE_SUBTITLE; export const VEHICLE_ROW_DESKTOP = - "grid w-full grid-cols-[minmax(7rem,1.2fr)_minmax(5rem,0.8fr)_minmax(6rem,0.9fr)_minmax(7rem,0.9fr)_minmax(7rem,1fr)_5.25rem] gap-4 items-center"; + "grid w-full grid-cols-[minmax(7rem,1.2fr)_minmax(5rem,0.8fr)_minmax(6rem,0.9fr)_minmax(10.5rem,0.9fr)_minmax(7rem,1fr)_5.25rem] gap-4 items-center"; export const VEHICLE_ROW_CELL = "min-w-0 font-normal text-[16px] leading-[1.5] text-[var(--edit-text-primary)] truncate"; @@ -175,6 +175,19 @@ export const VEHICLE_ROW_STATUS_TEXT_AVAILABLE = export const VEHICLE_ROW_STATUS_TEXT_IN_USE = "font-semibold text-[16px] leading-[22px] text-[var(--edit-text-secondary)] whitespace-nowrap"; +// Segmented toggle for Available / In use (Figma 8724:4604) +export const STATUS_TOGGLE_WRAPPER = + "bg-[var(--edit-stone-50)] flex gap-[4px] items-center p-[2px] rounded-[6px] w-fit"; + +export const STATUS_TOGGLE_BTN_ACTIVE = + "bg-[var(--edit-container-active)] flex items-center px-[8px] py-[6px] rounded-[4px] cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--edit-teal-300)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--edit-bg-primary)]"; + +export const STATUS_TOGGLE_BTN_INACTIVE = + "bg-transparent flex items-center px-[8px] py-[6px] rounded-[4px] cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--edit-teal-300)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--edit-bg-primary)]"; + +export const STATUS_TOGGLE_TEXT = + "font-semibold text-[16px] leading-[22px] text-[var(--edit-text-primary)] whitespace-nowrap"; + export const VEHICLE_ROW_ACTIONS = "flex items-center justify-end gap-1"; export const VEHICLE_ROW_ICON_BUTTON = From 3d5adeddd3606a180687d2cbf5e3b2948fb9d6f9 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 13:12:21 -0700 Subject: [PATCH 005/119] feat(edit): created UI component that shows overlay when user is dragging in files to import --- .../components/shared/DragDropOverlay.tsx | 28 +++++++++++++++++++ app/ui/src/app/edit/edit.module.css | 3 ++ app/ui/src/app/edit/formStyles.v2.ts | 15 +++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 app/ui/src/app/edit/components/shared/DragDropOverlay.tsx diff --git a/app/ui/src/app/edit/components/shared/DragDropOverlay.tsx b/app/ui/src/app/edit/components/shared/DragDropOverlay.tsx new file mode 100644 index 00000000..d626f55b --- /dev/null +++ b/app/ui/src/app/edit/components/shared/DragDropOverlay.tsx @@ -0,0 +1,28 @@ +import { + DRAG_DROP_OVERLAY_CONTENT, + DRAG_DROP_OVERLAY_ICON, + DRAG_DROP_OVERLAY_LABEL, + DRAG_DROP_OVERLAY_ROOT, +} from "@/app/edit/formStyles.v2"; + +export default function DragDropOverlay() { + return ( + + ); +} diff --git a/app/ui/src/app/edit/edit.module.css b/app/ui/src/app/edit/edit.module.css index 01f07bed..c49c9f2a 100644 --- a/app/ui/src/app/edit/edit.module.css +++ b/app/ui/src/app/edit/edit.module.css @@ -77,6 +77,9 @@ --edit-vehicle-light: #fcd34d; --edit-vehicle-back-panel: #312e81; + /* ── Drag-drop overlay ──────────────────────────────────────── */ + --edit-drag-overlay-bg: rgba(213, 242, 232, 0.48); + /* Address illustration */ --edit-address-accent: #f97316; --edit-address-on-accent: #ffffff; diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index b8927833..c15defb1 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -120,7 +120,7 @@ export const PAGE_V2_ROOT = export const PAGE_V2_BODY = "flex flex-1 lg:min-h-0 lg:overflow-hidden"; export const PAGE_V2_MAIN = - "flex-1 min-w-0 bg-[var(--edit-bg-primary)] border-t-0 lg:border-t border-l-0 lg:border-l border-[var(--edit-stone-200)] rounded-none lg:rounded-tl-[12px] p-6 pb-[136px] lg:pb-6 lg:overflow-y-auto space-y-16"; + "relative flex-1 min-w-0 bg-[var(--edit-bg-primary)] border-t-0 lg:border-t border-l-0 lg:border-l border-[var(--edit-stone-200)] rounded-none lg:rounded-tl-[12px] p-6 pb-[136px] lg:pb-6 lg:overflow-y-auto space-y-16"; export const VEHICLE_INFO_CONTAINER = "hidden lg:flex flex-col gap-4 border border-[var(--edit-stone-200)] rounded-[8px] overflow-hidden p-4"; @@ -650,6 +650,19 @@ export const MOBILE_FOOTER_TEXT_LINE = "relative shrink-0"; export const OPTIMIZING_SPINNER_WRAP = "flex justify-center mt-2"; +// ── Drag-Drop Overlay (Figma 8080:3134) ────────────────────────────────────── + +export const DRAG_DROP_OVERLAY_ROOT = + "absolute inset-0 z-50 flex items-center justify-center bg-[var(--edit-drag-overlay-bg)] border-4 border-[var(--edit-teal-600)] rounded-tl-[12px]"; + +export const DRAG_DROP_OVERLAY_CONTENT = + "flex flex-col gap-4 items-center w-[250px]"; + +export const DRAG_DROP_OVERLAY_ICON = "size-20 shrink-0"; + +export const DRAG_DROP_OVERLAY_LABEL = + "font-bold text-[20px] leading-[28px] text-[var(--edit-text-primary)] text-center"; + // ── Mobile Address Card Edit State (Figma 8325:4843) ────────────────────────── export const MOBILE_ADDR_CARD_EDIT_CONTENT = "flex flex-col gap-6"; From 6852f0d2fd41d86da471e0386c942d6870220ed2 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 13:43:12 -0700 Subject: [PATCH 006/119] style(edit): add manage header, change optimize button location, and make vehicle and delivery subheaders smaller --- .../components/layout/ManageSectionHeader.tsx | 30 ++++++++++++++++ .../components/vehicle/VehicleSection.tsx | 20 +---------- app/ui/src/app/edit/edit.module.css | 1 + app/ui/src/app/edit/formStyles.v2.ts | 36 +++++++++++-------- app/ui/src/app/edit/page.tsx | 20 +++++++---- 5 files changed, 67 insertions(+), 40 deletions(-) create mode 100644 app/ui/src/app/edit/components/layout/ManageSectionHeader.tsx diff --git a/app/ui/src/app/edit/components/layout/ManageSectionHeader.tsx b/app/ui/src/app/edit/components/layout/ManageSectionHeader.tsx new file mode 100644 index 00000000..cdea3a5e --- /dev/null +++ b/app/ui/src/app/edit/components/layout/ManageSectionHeader.tsx @@ -0,0 +1,30 @@ +"use client"; + +import styles from "@/app/edit/edit.module.css"; +import { + MANAGE_SECTION_HEADER_ROOT, + MANAGE_SECTION_HEADING, + VEHICLE_SECTION_OPTIMIZE_BTN, +} from "@/app/edit/formStyles.v2"; + +type Props = { + onOptimize: () => void; + isOptimizing: boolean; +}; + +export default function ManageSectionHeader({ onOptimize, isOptimizing }: Props) { + return ( +
+

Manage

+ +
+ ); +} diff --git a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx index 2df10d74..98f2f032 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx @@ -5,7 +5,6 @@ */ import { useState } from "react"; -import styles from "@/app/edit/edit.module.css"; import VehicleRow from "@/app/edit/components/vehicle/VehicleRow"; import VehicleEmptyState from "@/app/edit/components/vehicle/VehicleEmptyState"; import VehicleDetailsOverlay from "@/app/edit/components/vehicle/VehicleDetailsOverlay"; @@ -23,8 +22,6 @@ import { VEHICLE_SECTION_ACTIONS, VEHICLE_SECTION_HEADER, VEHICLE_SECTION_HEADING, - VEHICLE_SECTION_HEADING_ROW, - VEHICLE_SECTION_OPTIMIZE_BTN, VEHICLE_SECTION_SUBHEADING, MOBILE_EMPTY_STATE_CONTAINER, VEHICLE_MOBILE_LIST, @@ -69,8 +66,6 @@ type VehicleSectionProps = { activeVehicleIsValid: boolean; geocodeFailedVehicleIds: number[]; outOfRegionVehicleIds: number[]; - onOptimize: () => void; - isOptimizing: boolean; }; export default function VehicleSection({ @@ -84,8 +79,6 @@ export default function VehicleSection({ activeVehicleIsValid, geocodeFailedVehicleIds, outOfRegionVehicleIds, - onOptimize, - isOptimizing, }: VehicleSectionProps) { const [isAddOverlayOpen, setIsAddOverlayOpen] = useState(false); const [editingVehicle, setEditingVehicle] = useState( @@ -117,18 +110,7 @@ export default function VehicleSection({ return (
-
-

Vehicle details

- -
+

Vehicle details

Manage your delivery fleet

diff --git a/app/ui/src/app/edit/edit.module.css b/app/ui/src/app/edit/edit.module.css index c49c9f2a..42704e0c 100644 --- a/app/ui/src/app/edit/edit.module.css +++ b/app/ui/src/app/edit/edit.module.css @@ -35,6 +35,7 @@ --edit-container-active: #d5f2e8; --edit-text-primary: #272725; --edit-text-secondary: #464544; + --edit-manage-heading: #000000; /* ── Buttons ──────────────────────────────────────────────────── */ --edit-btn-primary: #4cb599; diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index c15defb1..c44a2442 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -109,7 +109,7 @@ export const NAVBAR_V2_BTN_SAVE = "h-9 px-[16px] rounded-[5px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; export const NAVBAR_V2_BTN_OUTLINE = - "h-9 px-4 rounded-[80px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"; + "h-9 px-4 rounded-[5px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"; export const NAVBAR_V2_BTN_FILLED = "h-9 px-4 rounded-[80px] bg-[var(--edit-btn-primary)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap cursor-pointer disabled:cursor-not-allowed"; @@ -232,38 +232,46 @@ export const VEHICLE_SECTION_OPTIMIZE_BTN = export const VEHICLE_SECTION_HEADER = "flex flex-col gap-2 mb-4"; export const VEHICLE_SECTION_HEADING = - "font-bold text-[20px] leading-[28px] text-[var(--edit-text-primary)]"; + "font-[650] text-[16px] leading-[1.5] text-[var(--edit-text-primary)]"; export const VEHICLE_SECTION_SUBHEADING = - "text-[16px] leading-normal text-[var(--edit-text-secondary)]"; + "font-normal text-[16px] leading-[1.5] text-[var(--edit-text-secondary)] whitespace-nowrap"; + +export const MANAGE_SECTION_HEADER_ROOT = + "flex items-center justify-between w-full"; + +export const MANAGE_SECTION_HEADING = + "font-bold text-[20px] leading-[28px] text-[var(--edit-manage-heading)] whitespace-nowrap"; + +export const MANAGE_VEHICLE_GROUP = "flex flex-col gap-4"; export const ADDRESS_SECTION_WITH_PAGINATION = "flex flex-col gap-4"; export const ADDRESS_SECTION_HEADER = "flex flex-col gap-2 mb-4"; export const ADDRESS_SECTION_HEADING = - "font-bold text-[20px] leading-[28px] text-[var(--edit-text-primary)]"; + "font-[650] text-[16px] leading-[1.5] text-[var(--edit-text-primary)]"; export const ADDRESS_SECTION_SUBHEADING = - "text-[16px] leading-normal text-[var(--edit-text-secondary)]"; + "font-normal text-[16px] leading-[1.5] text-[var(--edit-text-secondary)] whitespace-nowrap"; export const ADDRESS_BTN_V2_DESKTOP_ENABLED = - "h-10 px-4 shrink-0 rounded-[80px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; + "h-10 px-4 shrink-0 rounded-[5px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; export const ADDRESS_BTN_V2_DESKTOP_DISABLED = - "h-10 px-4 shrink-0 rounded-[80px] border border-[var(--edit-stone-200)] font-semibold text-[14px] leading-5 text-[var(--edit-stone-500)] whitespace-nowrap cursor-not-allowed opacity-50"; + "h-10 px-4 shrink-0 rounded-[5px] border border-[var(--edit-stone-200)] font-semibold text-[14px] leading-5 text-[var(--edit-stone-500)] whitespace-nowrap overflow-clip cursor-not-allowed opacity-50"; export const ADDRESS_BTN_V2_MOBILE_ENABLED = - "w-full h-10 px-4 rounded-[80px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; + "w-full h-10 px-4 rounded-[5px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; export const ADDRESS_BTN_V2_MOBILE_DISABLED = - "w-full h-10 px-4 rounded-[80px] border border-[var(--edit-stone-200)] font-semibold text-[14px] leading-5 text-[var(--edit-stone-500)] whitespace-nowrap cursor-not-allowed opacity-50"; + "w-full h-10 px-4 rounded-[5px] border border-[var(--edit-stone-200)] font-semibold text-[14px] leading-5 text-[var(--edit-stone-500)] whitespace-nowrap overflow-clip cursor-not-allowed opacity-50"; export const ADDRESS_SEARCH_BAR = - "flex items-center gap-2 px-4 py-[11px] rounded-[80px] border border-[var(--edit-stone-200)] bg-[var(--edit-stone-50)] focus-within:border-[var(--edit-teal-300)] transition-colors"; + "flex items-center gap-2 px-4 py-[11px] rounded-[4px] border border-[var(--edit-stone-200)] bg-[var(--edit-stone-50)] focus-within:border-[var(--edit-teal-300)] transition-colors"; export const ADDRESS_SEARCH_BAR_DESKTOP = - "flex items-center gap-2 h-9 px-4 rounded-[80px] border border-[var(--edit-stone-200)] bg-transparent focus-within:border-[var(--edit-teal-300)] transition-colors"; + "flex items-center gap-2 h-9 px-4 rounded-[4px] border border-[var(--edit-stone-200)] bg-transparent focus-within:border-[var(--edit-teal-300)] transition-colors"; export const ADDRESS_SEARCH_INPUT = "flex-1 font-normal text-[16px] leading-[1.5] text-[var(--edit-text-primary)] placeholder:text-[var(--edit-stone-500)] outline-none bg-transparent min-w-0 [&::-webkit-search-cancel-button]:hidden"; @@ -284,13 +292,13 @@ export const MOBILE_ADDR_TOOLBAR_ROOT = export const MOBILE_ADDR_TOOLBAR_BTN_ROW = "flex gap-2 items-center"; export const MOBILE_ADDR_TOOLBAR_BTN_ENABLED = - "h-9 px-4 shrink-0 rounded-[80px] border border-[var(--edit-text-primary)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; + "h-9 px-4 shrink-0 rounded-[5px] border border-[var(--edit-text-primary)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; export const MOBILE_ADDR_TOOLBAR_BTN_DISABLED = - "h-9 px-4 shrink-0 rounded-[80px] border border-[var(--edit-stone-200)] font-semibold text-[14px] leading-5 text-[var(--edit-stone-500)] whitespace-nowrap cursor-not-allowed opacity-50"; + "h-9 px-4 shrink-0 rounded-[5px] border border-[var(--edit-stone-200)] font-semibold text-[14px] leading-5 text-[var(--edit-stone-500)] whitespace-nowrap overflow-clip cursor-not-allowed opacity-50"; export const ADDRESS_SEARCH_BAR_COMPACT = - "h-9 w-full flex items-center gap-2 px-4 rounded-[80px] border border-[var(--edit-stone-200)] focus-within:border-[var(--edit-teal-300)] transition-colors"; + "h-9 w-full flex items-center gap-2 px-4 rounded-[4px] border border-[var(--edit-stone-200)] focus-within:border-[var(--edit-teal-300)] transition-colors"; // ── Address Row Header (Figma 8012:2303) ────────────────────────────────────── diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index 2d587393..d1ceaac0 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -19,8 +19,10 @@ import { PAGE_V2_BODY, PAGE_V2_MAIN, ADDRESS_SECTION_WITH_PAGINATION, + MANAGE_VEHICLE_GROUP, } from "@/app/edit/formStyles.v2"; import VehicleSection from "@/app/edit/components/vehicle/VehicleSection"; +import ManageSectionHeader from "@/app/edit/components/layout/ManageSectionHeader"; import AddressSection from "@/app/edit/components/address/AddressSection"; import AddressPagination from "@/app/edit/components/address/AddressPagination"; import AddressPaginationMobile from "@/app/edit/components/address/AddressPaginationMobile"; @@ -215,13 +217,17 @@ export default function Page() {
- void optimize()} - isOptimizing={isOptimizing} - /> +
+ void optimize()} + isOptimizing={isOptimizing} + /> + +
Date: Sat, 23 May 2026 18:25:16 -0700 Subject: [PATCH 007/119] refactor(edit): move manageSectionHeader to more appropriate folder --- .../edit/components/{layout => shared}/ManageSectionHeader.tsx | 0 app/ui/src/app/edit/page.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename app/ui/src/app/edit/components/{layout => shared}/ManageSectionHeader.tsx (100%) diff --git a/app/ui/src/app/edit/components/layout/ManageSectionHeader.tsx b/app/ui/src/app/edit/components/shared/ManageSectionHeader.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/ManageSectionHeader.tsx rename to app/ui/src/app/edit/components/shared/ManageSectionHeader.tsx diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index d1ceaac0..b7d948ed 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -22,7 +22,7 @@ import { MANAGE_VEHICLE_GROUP, } from "@/app/edit/formStyles.v2"; import VehicleSection from "@/app/edit/components/vehicle/VehicleSection"; -import ManageSectionHeader from "@/app/edit/components/layout/ManageSectionHeader"; +import ManageSectionHeader from "@/app/edit/components/shared/ManageSectionHeader"; import AddressSection from "@/app/edit/components/address/AddressSection"; import AddressPagination from "@/app/edit/components/address/AddressPagination"; import AddressPaginationMobile from "@/app/edit/components/address/AddressPaginationMobile"; From caf6cea3f74680fa4f9140aec4d6e44182999988 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 18:36:28 -0700 Subject: [PATCH 008/119] feat(edit): wire up new useCSVImport to address import and remove old useCSVUpload --- .../components/address/AddressSection.tsx | 9 +- app/ui/src/app/edit/hooks/useCSVUpload.ts | 161 ------------------ app/ui/src/app/edit/page.tsx | 18 +- 3 files changed, 14 insertions(+), 174 deletions(-) delete mode 100644 app/ui/src/app/edit/hooks/useCSVUpload.ts diff --git a/app/ui/src/app/edit/components/address/AddressSection.tsx b/app/ui/src/app/edit/components/address/AddressSection.tsx index 6089a7d6..1ef43e8c 100644 --- a/app/ui/src/app/edit/components/address/AddressSection.tsx +++ b/app/ui/src/app/edit/components/address/AddressSection.tsx @@ -54,7 +54,7 @@ type AddressSectionProps = { searchQuery: string; setSearchQuery: (q: string) => void; outOfRegionIds: number[]; - onCSVUpload: (event: React.ChangeEvent) => void; + onOpenImportModal: (file: File) => void; }; export default function AddressSection({ @@ -72,7 +72,7 @@ export default function AddressSection({ searchQuery, setSearchQuery, outOfRegionIds, - onCSVUpload, + onOpenImportModal, }: AddressSectionProps) { const fileInputRef = useRef(null); const [addressToDeleteId, setAddressToDeleteId] = useState( @@ -97,9 +97,10 @@ export default function AddressSection({ { - onCSVUpload(e); + const file = e.target.files?.[0]; + if (file) onOpenImportModal(file); e.target.value = ""; }} className="hidden" diff --git a/app/ui/src/app/edit/hooks/useCSVUpload.ts b/app/ui/src/app/edit/hooks/useCSVUpload.ts deleted file mode 100644 index 80b26c88..00000000 --- a/app/ui/src/app/edit/hooks/useCSVUpload.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * CSV upload hook: parses a CSV file into AddressCard[], - * then bulk-imports them into the edit page state. - */ - -import { useCallback, useState } from "react"; -import Papa from "papaparse"; -import type { AddressCard } from "@/app/edit/types/delivery"; -import { - resolveColumns, - normalizeTimeOption, - bufferSecondsToMinutes, -} from "@/app/edit/utils/csvParserUtils"; -import { hasAtLeastOneLetter } from "@/app/components/AddressGeocoder/utils"; -import { migrateSessionSaveFile } from "@/lib/validation/session.schema"; -import { mapOptimizeRequestToEditState } from "@/app/edit/utils/sessionMapper"; - -type UseCSVUploadArgs = { - importAddresses: (addresses: AddressCard[]) => void; -}; - -export function useCSVUpload({ importAddresses }: UseCSVUploadArgs) { - const [csvError, setCsvError] = useState(null); - - const handleCSVUpload = useCallback( - async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file) return; - - setCsvError(null); - - try { - const addresses = await parseAddressUpload( - file.name, - await file.text(), - ); - importAddresses(addresses); - } catch (err) { - setCsvError( - err instanceof Error - ? err.message - : "An unexpected error occurred while processing the upload.", - ); - } finally { - event.target.value = ""; - } - }, - [importAddresses], - ); - - const clearCsvError = useCallback(() => setCsvError(null), []); - - return { handleCSVUpload, csvError, clearCsvError }; -} - -export function parseAddressUpload( - fileName: string, - content: string, -): Promise { - const normalizedName = fileName.toLowerCase(); - - if (normalizedName.endsWith(".csv")) { - return parseCsvAddressUpload(content); - } - - if (normalizedName.endsWith(".json")) { - return parseJsonAddressUpload(content); - } - - throw new Error( - "Please upload a .csv file or an exported session .json file.", - ); -} - -function parseCsvAddressUpload(content: string): Promise { - return new Promise((resolve, reject) => { - Papa.parse>(content, { - header: true, - skipEmptyLines: true, - complete: (results) => { - try { - const addresses = mapCsvRowsToAddresses( - results.data, - results.meta.fields ?? [], - ); - resolve(addresses); - } catch (error) { - reject(error); - } - }, - error: (error: Error) => { - reject(new Error(`CSV parsing error: ${error.message}`)); - }, - }); - }); -} - -function parseJsonAddressUpload(content: string): Promise { - let parsed: unknown; - - try { - parsed = JSON.parse(content); - } catch { - throw new Error("This file is not valid JSON."); - } - - try { - const session = migrateSessionSaveFile(parsed).data; - return Promise.resolve(mapOptimizeRequestToEditState(session).addresses); - } catch (error) { - throw error instanceof Error - ? error - : new Error("JSON uploads must use the exported session save format."); - } -} - -function mapCsvRowsToAddresses( - rows: Record[], - fields: string[], -): AddressCard[] { - const cols = resolveColumns(fields); - if (!cols.address) { - throw new Error( - 'CSV must contain an "address" column (or similar: "delivery address", "street", "location", "destination").', - ); - } - - const get = (row: Record, key: string) => - (cols[key] ? row[cols[key]!]?.trim() : undefined) ?? ""; - - const addresses: AddressCard[] = []; - let addrId = 1; - - for (const row of rows) { - const address = get(row, "address"); - if (!address || !hasAtLeastOneLetter(address)) continue; - - const timeStart = normalizeTimeOption(get(row, "time_window_start")); - const timeEnd = normalizeTimeOption(get(row, "time_window_end")); - - addresses.push({ - id: addrId++, - locked: true, - editingExisting: false, - recipientName: get(row, "recipient_name"), - phoneNumber: get(row, "phone_number"), - recipientAddress: address, - timeBuffer: bufferSecondsToMinutes(get(row, "time_buffer")), - deliveryTimeStart: timeStart, - deliveryTimeEnd: timeEnd, - deliveryQuantity: parseInt(get(row, "demand_value") || "1", 10) || 1, - notes: get(row, "notes"), - }); - } - - if (addresses.length === 0) { - throw new Error("No valid deliveries found in the uploaded file."); - } - - return addresses; -} diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index b7d948ed..1886599c 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -33,7 +33,6 @@ import { CSVImportModal } from "@/app/edit/components/CSVImportModal"; import { useVehicles } from "@/app/edit/hooks/useVehicles"; import { useAddresses } from "@/app/edit/hooks/useAddresses"; import { useOptimize } from "@/app/edit/hooks/useOptimize"; -import { useCSVUpload } from "@/app/edit/hooks/useCSVUpload"; import { useCSVImport } from "@/app/edit/hooks/useCSVImport"; import { useCallback, useEffect, useState } from "react"; import type { AddressCard } from "@/app/edit/types/delivery"; @@ -77,13 +76,13 @@ export default function Page() { addressState.cacheAddressLocation, ); - const { handleCSVUpload, csvError, clearCsvError } = useCSVUpload({ - importAddresses: addressState.importAddresses, - }); - - // In-page modal for CSV/JSON imports triggered from AddressSection - const { csvData, isImportModalOpen, parseError, closeImportModal } = - useCSVImport(); + const { + csvData, + isImportModalOpen, + parseError, + openImportModal, + closeImportModal, + } = useCSVImport(); useEffect(() => { let cancelled = false; @@ -196,6 +195,7 @@ export default function Page() { )} + {needsDepotAddress && ( From 652cc6a1c7185f541c80b094f1139f35a9a63691 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 18:45:47 -0700 Subject: [PATCH 009/119] refactor(edit): move CSVImportModal to the address folder --- .../src/app/edit/components/{ => address}/CSVImportModal.tsx | 4 ++-- app/ui/src/app/edit/page.tsx | 4 ++-- app/ui/src/app/upload-save-point/page.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename app/ui/src/app/edit/components/{ => address}/CSVImportModal.tsx (99%) diff --git a/app/ui/src/app/edit/components/CSVImportModal.tsx b/app/ui/src/app/edit/components/address/CSVImportModal.tsx similarity index 99% rename from app/ui/src/app/edit/components/CSVImportModal.tsx rename to app/ui/src/app/edit/components/address/CSVImportModal.tsx index 57a40466..8fd8d0ad 100644 --- a/app/ui/src/app/edit/components/CSVImportModal.tsx +++ b/app/ui/src/app/edit/components/address/CSVImportModal.tsx @@ -1,4 +1,4 @@ -// app/edit/components/CSVImportModal.tsx +// app/edit/components/address/CSVImportModal.tsx "use client"; /** @@ -18,7 +18,7 @@ import { useState, useCallback, useMemo } from "react"; import { useRouter } from "next/navigation"; -import type { AddressCard } from "../types/delivery"; +import type { AddressCard } from "@/app/edit/types/delivery"; // ─── Types ──────────────────────────────────────────────────────────────────── diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index 1886599c..d8778a51 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -29,7 +29,7 @@ import AddressPaginationMobile from "@/app/edit/components/address/AddressPagina import EditPageFooter from "@/app/edit/components/layout/footer/EditPageFooter"; import MobileEditPageFooter from "@/app/edit/components/layout/footer/MobileEditPageFooter"; import MobileBottomBar from "@/app/edit/components/layout/navbar/MobileBottomBar"; -import { CSVImportModal } from "@/app/edit/components/CSVImportModal"; +import { CSVImportModal } from "@/app/edit/components/address/CSVImportModal"; import { useVehicles } from "@/app/edit/hooks/useVehicles"; import { useAddresses } from "@/app/edit/hooks/useAddresses"; import { useOptimize } from "@/app/edit/hooks/useOptimize"; @@ -188,7 +188,7 @@ export default function Page() { + importAddresses={(cards: AddressCard[]) => addressState.importAddresses(reindexAddresses(cards)) } /> diff --git a/app/ui/src/app/upload-save-point/page.tsx b/app/ui/src/app/upload-save-point/page.tsx index 0f76d4d3..d4d7a821 100644 --- a/app/ui/src/app/upload-save-point/page.tsx +++ b/app/ui/src/app/upload-save-point/page.tsx @@ -6,7 +6,7 @@ export const dynamic = "force-dynamic"; import { useState, useRef, useCallback } from "react"; import { useRouter } from "next/navigation"; import ShellNavbar from "@/app/components/ShellNavbar"; -import { CSVImportModal } from "@/app/edit/components/CSVImportModal"; +import { CSVImportModal } from "@/app/edit/components/address/CSVImportModal"; import { useCSVImport } from "@/app/edit/hooks/useCSVImport"; import { migrateSessionSaveFile } from "@/lib/validation/session.schema"; import { formatSize } from "@/app/utils/routeUtils"; From 60f80241ec78051c79dcdc86cf31d8e38bc40729 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 19:06:51 -0700 Subject: [PATCH 010/119] fix(edit): remove unused imports and wire up save button --- app/ui/src/app/edit/page.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index d8778a51..7b0a5f1f 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -42,7 +42,6 @@ import { mapEditStateToOptimizeRequest, mapOptimizeRequestToEditState, } from "@/app/edit/utils/sessionMapper"; -import { useRouter } from "next/navigation"; import AddressOverlay, { type LocationAddress, } from "@/app/edit/components/address/AddressOverlay"; @@ -50,7 +49,6 @@ import AddressOverlay, { type StoredUploadFile = { name: string; content: string }; export default function Page() { - const router = useRouter(); const vehicleState = useVehicles(); const addressState = useAddresses(); const [sessionError, setSessionError] = useState(null); @@ -144,12 +142,6 @@ export default function Page() { }; }, [importAddresses, importVehicles]); - // Routes to /upload-save-point so the user can upload a .json save file - // or a .csv/.json address list through the column-mapper modal flow. - const handleImportSession = useCallback(() => { - router.push("/upload-save-point"); - }, [router]); - const handleExportSession = useCallback(async () => { setSessionError(null); try { @@ -196,6 +188,7 @@ export default function Page() { + {needsDepotAddress && ( setIsMobileMenuOpen(false)} /> - {}} /> + setIsMobileMenuOpen(true)} /> - {}} /> +
From d0635ad57c82f5ae9710f4a2382dc488c187d9d4 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 19:36:29 -0700 Subject: [PATCH 011/119] fix(edit): address start location not showing in results page by wiring up the start location to be displayed in the results page --- app/ui/src/app/edit/hooks/useOptimize.ts | 1 + app/ui/src/app/edit/utils/vroomToRoutes.ts | 12 +++++ app/ui/src/app/results/components/Map.tsx | 46 ++++++++++++++++++- app/ui/src/app/results/components/Sidebar.tsx | 17 +++++++ app/ui/src/app/results/types.ts | 1 + 5 files changed, 76 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/hooks/useOptimize.ts b/app/ui/src/app/edit/hooks/useOptimize.ts index 9c3998ca..8b3a86ba 100644 --- a/app/ui/src/app/edit/hooks/useOptimize.ts +++ b/app/ui/src/app/edit/hooks/useOptimize.ts @@ -361,6 +361,7 @@ export function useOptimize( resultBody as VroomResponse, lockedVehicles, addresses, + trimmedDepotAddress!, ); sessionStorage.setItem("optimizeResults", JSON.stringify(routes)); router.push("/results"); diff --git a/app/ui/src/app/edit/utils/vroomToRoutes.ts b/app/ui/src/app/edit/utils/vroomToRoutes.ts index 8eee8170..1f58bd04 100644 --- a/app/ui/src/app/edit/utils/vroomToRoutes.ts +++ b/app/ui/src/app/edit/utils/vroomToRoutes.ts @@ -38,6 +38,7 @@ export function vroomToRoutes( vroomResponse: VroomResponse, vehicles: VehicleRow[], addresses: AddressCard[], + depotAddress: string, ): Route[] { const vehicleById = new Map(vehicles.map((v) => [String(v.id), v])); const addressById = new Map(addresses.map((a) => [String(a.id), a])); @@ -45,6 +46,10 @@ export function vroomToRoutes( return vroomResponse.routes.map((vroomRoute: VroomRoute): Route => { const vehicle = vehicleById.get(vroomRoute.vehicle_external_id); + const startStep = vroomRoute.steps.find( + (s: VroomStep) => s.type === "start", + ); + const jobSteps = vroomRoute.steps.filter( (s: VroomStep) => s.type === "job" && s.job_external_id != null, ); @@ -85,6 +90,13 @@ export function vroomToRoutes( vehicleType: vehicle?.type || undefined, distanceMi: Math.round(vroomRoute.distance * METERS_TO_MILES * 10) / 10, estimatedTimeMinutes: Math.round(vroomRoute.duration / 60), + startLocation: startStep + ? { + lat: startStep.location[1], + lng: startStep.location[0], + address: vehicle?.startLocation || depotAddress || "", + } + : undefined, }; }); } diff --git a/app/ui/src/app/results/components/Map.tsx b/app/ui/src/app/results/components/Map.tsx index 6d03ad37..a90b96c8 100644 --- a/app/ui/src/app/results/components/Map.tsx +++ b/app/ui/src/app/results/components/Map.tsx @@ -57,7 +57,7 @@ function buildRoutePath( pendingPinMove: PendingPinMove | null, ): google.maps.LatLngLiteral[] { const sorted = [...route.stops].sort((a, b) => a.sequence - b.sequence); - return sorted.map((s) => { + const deliveryPoints = sorted.map((s) => { if ( pendingPinMove?.vehicleId === route.vehicleId && pendingPinMove.stopId === s.id @@ -66,6 +66,13 @@ function buildRoutePath( } return { lat: s.lat, lng: s.lng }; }); + if (route.startLocation) { + return [ + { lat: route.startLocation.lat, lng: route.startLocation.lng }, + ...deliveryPoints, + ]; + } + return deliveryPoints; } function RoutePolylinesOverlay({ @@ -308,6 +315,25 @@ function AdvancedMarkers({ if (cancelled) return; routes.forEach((route) => { + // Depot marker — distinct non-draggable pin labeled "S" + if (route.startLocation) { + const depotEl = document.createElement("div"); + depotEl.style.cssText = + "display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;background:#374151;color:#fff;font-size:11px;font-weight:700;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,0.4)"; + depotEl.textContent = "S"; + const depotMarker = new AdvancedMarkerElement({ + map, + position: { + lat: route.startLocation.lat, + lng: route.startLocation.lng, + }, + title: route.startLocation.address || "Starting point", + content: depotEl, + gmpDraggable: false, + }); + markers.push(depotMarker); + } + const sorted = [...route.stops].sort( (a, b) => a.sequence - b.sequence, ); @@ -403,6 +429,12 @@ export default function MapComponent({ const bounds = new google.maps.LatLngBounds(); routes.forEach((route) => { route.stops.forEach((s) => bounds.extend({ lat: s.lat, lng: s.lng })); + if (route.startLocation) { + bounds.extend({ + lat: route.startLocation.lat, + lng: route.startLocation.lng, + }); + } }); mapInstance.fitBounds(bounds, 48); }, @@ -472,6 +504,18 @@ export default function MapComponent({ ); return ( + {route.startLocation && ( + + )} {sorted.map((stop) => { const atPending = pendingPinMove != null && diff --git a/app/ui/src/app/results/components/Sidebar.tsx b/app/ui/src/app/results/components/Sidebar.tsx index 76a6bed9..cd49b43e 100644 --- a/app/ui/src/app/results/components/Sidebar.tsx +++ b/app/ui/src/app/results/components/Sidebar.tsx @@ -173,6 +173,23 @@ export default function Sidebar({ {isExpanded && (
    + {route.startLocation && ( +
  • +
    + + S + +
    +

    + Starting point +

    +

    + {route.startLocation.address || "Depot"} +

    +
    +
    +
  • + )} {sortedStops.map((stop) => (
  • Date: Sat, 23 May 2026 21:18:08 -0700 Subject: [PATCH 012/119] refactor(edit): move sidebar and navbar outside of edit page so results page can integrate both components --- .../navbar/MobileBottomBar.tsx | 0 .../navbar/MobileNavbar.tsx | 0 .../layout => components}/navbar/Navbar.tsx | 0 .../sidebar/MobileSidebar.tsx | 0 .../layout => components}/sidebar/Sidebar.tsx | 0 .../sidebar/SidebarEditButton.tsx | 0 .../sidebar/SidebarResultsButton.tsx | 0 .../{layout => }/footer/EditPageFooter.tsx | 0 .../footer/MobileEditPageFooter.tsx | 0 app/ui/src/app/edit/page.tsx | 18 +++++++++--------- 10 files changed, 9 insertions(+), 9 deletions(-) rename app/ui/src/app/{edit/components/layout => components}/navbar/MobileBottomBar.tsx (100%) rename app/ui/src/app/{edit/components/layout => components}/navbar/MobileNavbar.tsx (100%) rename app/ui/src/app/{edit/components/layout => components}/navbar/Navbar.tsx (100%) rename app/ui/src/app/{edit/components/layout => components}/sidebar/MobileSidebar.tsx (100%) rename app/ui/src/app/{edit/components/layout => components}/sidebar/Sidebar.tsx (100%) rename app/ui/src/app/{edit/components/layout => components}/sidebar/SidebarEditButton.tsx (100%) rename app/ui/src/app/{edit/components/layout => components}/sidebar/SidebarResultsButton.tsx (100%) rename app/ui/src/app/edit/components/{layout => }/footer/EditPageFooter.tsx (100%) rename app/ui/src/app/edit/components/{layout => }/footer/MobileEditPageFooter.tsx (100%) diff --git a/app/ui/src/app/edit/components/layout/navbar/MobileBottomBar.tsx b/app/ui/src/app/components/navbar/MobileBottomBar.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/navbar/MobileBottomBar.tsx rename to app/ui/src/app/components/navbar/MobileBottomBar.tsx diff --git a/app/ui/src/app/edit/components/layout/navbar/MobileNavbar.tsx b/app/ui/src/app/components/navbar/MobileNavbar.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/navbar/MobileNavbar.tsx rename to app/ui/src/app/components/navbar/MobileNavbar.tsx diff --git a/app/ui/src/app/edit/components/layout/navbar/Navbar.tsx b/app/ui/src/app/components/navbar/Navbar.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/navbar/Navbar.tsx rename to app/ui/src/app/components/navbar/Navbar.tsx diff --git a/app/ui/src/app/edit/components/layout/sidebar/MobileSidebar.tsx b/app/ui/src/app/components/sidebar/MobileSidebar.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/sidebar/MobileSidebar.tsx rename to app/ui/src/app/components/sidebar/MobileSidebar.tsx diff --git a/app/ui/src/app/edit/components/layout/sidebar/Sidebar.tsx b/app/ui/src/app/components/sidebar/Sidebar.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/sidebar/Sidebar.tsx rename to app/ui/src/app/components/sidebar/Sidebar.tsx diff --git a/app/ui/src/app/edit/components/layout/sidebar/SidebarEditButton.tsx b/app/ui/src/app/components/sidebar/SidebarEditButton.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/sidebar/SidebarEditButton.tsx rename to app/ui/src/app/components/sidebar/SidebarEditButton.tsx diff --git a/app/ui/src/app/edit/components/layout/sidebar/SidebarResultsButton.tsx b/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/sidebar/SidebarResultsButton.tsx rename to app/ui/src/app/components/sidebar/SidebarResultsButton.tsx diff --git a/app/ui/src/app/edit/components/layout/footer/EditPageFooter.tsx b/app/ui/src/app/edit/components/footer/EditPageFooter.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/footer/EditPageFooter.tsx rename to app/ui/src/app/edit/components/footer/EditPageFooter.tsx diff --git a/app/ui/src/app/edit/components/layout/footer/MobileEditPageFooter.tsx b/app/ui/src/app/edit/components/footer/MobileEditPageFooter.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/footer/MobileEditPageFooter.tsx rename to app/ui/src/app/edit/components/footer/MobileEditPageFooter.tsx diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index 7b0a5f1f..378c58d4 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -6,14 +6,14 @@ */ import styles from "@/app/edit/edit.module.css"; -import Navbar from "@/app/edit/components/layout/navbar/Navbar"; -import MobileNavbar from "@/app/edit/components/layout/navbar/MobileNavbar"; -import MobileSidebar from "@/app/edit/components/layout/sidebar/MobileSidebar"; +import Navbar from "@/app/components/navbar/Navbar"; +import MobileNavbar from "@/app/components/navbar/MobileNavbar"; +import MobileSidebar from "@/app/components/sidebar/MobileSidebar"; import OptimizingModal from "@/app/edit/components/shared/OptimizingModal"; import ErrorPopup from "@/app/edit/components/shared/ErrorPopup"; -import Sidebar from "@/app/edit/components/layout/sidebar/Sidebar"; -import SidebarEditButton from "@/app/edit/components/layout/sidebar/SidebarEditButton"; -import SidebarResultsButton from "@/app/edit/components/layout/sidebar/SidebarResultsButton"; +import Sidebar from "@/app/components/sidebar/Sidebar"; +import SidebarEditButton from "@/app/components/sidebar/SidebarEditButton"; +import SidebarResultsButton from "@/app/components/sidebar/SidebarResultsButton"; import { PAGE_V2_ROOT, PAGE_V2_BODY, @@ -26,9 +26,9 @@ import ManageSectionHeader from "@/app/edit/components/shared/ManageSectionHeade import AddressSection from "@/app/edit/components/address/AddressSection"; import AddressPagination from "@/app/edit/components/address/AddressPagination"; import AddressPaginationMobile from "@/app/edit/components/address/AddressPaginationMobile"; -import EditPageFooter from "@/app/edit/components/layout/footer/EditPageFooter"; -import MobileEditPageFooter from "@/app/edit/components/layout/footer/MobileEditPageFooter"; -import MobileBottomBar from "@/app/edit/components/layout/navbar/MobileBottomBar"; +import EditPageFooter from "@/app/edit/components/footer/EditPageFooter"; +import MobileEditPageFooter from "@/app/edit/components/footer/MobileEditPageFooter"; +import MobileBottomBar from "@/app/components/navbar/MobileBottomBar"; import { CSVImportModal } from "@/app/edit/components/address/CSVImportModal"; import { useVehicles } from "@/app/edit/hooks/useVehicles"; import { useAddresses } from "@/app/edit/hooks/useAddresses"; From 886f8c141b6372a932a37a4569e8bcb8d32c8867 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 22:06:37 -0700 Subject: [PATCH 013/119] feat(edit): add inline errors for the addressCard --- .../edit/components/address/AddressCard.tsx | 71 ++++++++++--------- .../components/address/AddressOverlay.tsx | 12 ++-- .../{OverlayFieldError.tsx => FieldError.tsx} | 4 +- .../vehicle/VehicleDetailsOverlay.tsx | 12 ++-- app/ui/src/app/edit/formStyles.v2.ts | 4 +- 5 files changed, 53 insertions(+), 50 deletions(-) rename app/ui/src/app/edit/components/shared/{OverlayFieldError.tsx => FieldError.tsx} (88%) diff --git a/app/ui/src/app/edit/components/address/AddressCard.tsx b/app/ui/src/app/edit/components/address/AddressCard.tsx index fa43eca7..8c602e5c 100644 --- a/app/ui/src/app/edit/components/address/AddressCard.tsx +++ b/app/ui/src/app/edit/components/address/AddressCard.tsx @@ -15,7 +15,6 @@ import { ADDRESS_ROW_NAME_ROW, ADDRESS_ROW_FIELD_INPUT_FILL, ADDRESS_ROW_ADDR_WRAP, - ADDRESS_ROW_ADDR_WRAP_ERROR, ADDRESS_ROW_ADDR_GRADIENT, ADDRESS_ROW_ADDR_TRIGGER_TEXT, ADDRESS_ROW_ADDR_TRIGGER_PLACEHOLDER, @@ -69,7 +68,9 @@ import { ADDRESS_CARD_MOBILE_ROOT, MOBILE_ADDR_EXPANDED_PANEL, MOBILE_ADDR_LOCKED_NOTES_CLAMP, + ADDRESS_ROW_QTY_FIELD_COL, } from "@/app/edit/formStyles.v2"; +import FieldError from "@/app/edit/components/shared/FieldError"; import { EditIconButton, ConfirmIconButton, @@ -239,8 +240,8 @@ export default function AddressCard({ const startIdx = TIME_OPTIONS.indexOf(a.deliveryTimeStart); const endIdx = TIME_OPTIONS.indexOf(a.deliveryTimeEnd); - const addrInvalid = - geocodeFailed || (addressTouched && !a.recipientAddress.trim()); + const addressMissing = addressTouched && !a.recipientAddress.trim(); + const qtyInvalid = addressTouched && a.deliveryQuantity <= 0; const panelId = `addr-panel-${a.id}`; @@ -355,11 +356,7 @@ export default function AddressCard({
+ {addressMissing && ( + + )}
{/* Quantity — edit */} - updateAddress(a.id, "deliveryQuantity", v)} - onIncrement={() => - updateAddress( - a.id, - "deliveryQuantity", - Math.min(1_000_000, (a.deliveryQuantity || 0) + 1), - ) - } - onDecrement={() => - updateAddress( - a.id, - "deliveryQuantity", - Math.max(1, (a.deliveryQuantity || 1) - 1), - ) - } - /> +
+ updateAddress(a.id, "deliveryQuantity", v)} + onIncrement={() => + updateAddress( + a.id, + "deliveryQuantity", + Math.min(1_000_000, (a.deliveryQuantity || 0) + 1), + ) + } + onDecrement={() => + updateAddress( + a.id, + "deliveryQuantity", + Math.max(1, (a.deliveryQuantity || 1) - 1), + ) + } + /> + {qtyInvalid && } +
{/* Delivery estimation — edit */}
@@ -763,11 +766,7 @@ export default function AddressCard({
+ {addressMissing && ( + + )}
{/* Delivery Info */} @@ -817,6 +819,7 @@ export default function AddressCard({ ) } /> + {qtyInvalid && }
diff --git a/app/ui/src/app/edit/components/address/AddressOverlay.tsx b/app/ui/src/app/edit/components/address/AddressOverlay.tsx index fe74cbe1..d88d9c51 100644 --- a/app/ui/src/app/edit/components/address/AddressOverlay.tsx +++ b/app/ui/src/app/edit/components/address/AddressOverlay.tsx @@ -10,7 +10,7 @@ import { import type { CSSProperties } from "react"; import { useFocusTrap } from "@/app/edit/hooks/useFocusTrap"; import { SUPPORTED_STATES } from "@/app/edit/constants/supportedRegions"; -import OverlayFieldError from "@/app/edit/components/shared/OverlayFieldError"; +import FieldError from "@/app/edit/components/shared/FieldError"; import OverlayAutocompleteDropdown from "@/app/edit/components/shared/OverlayAutocompleteDropdown"; import { useAddressAutocomplete } from "@/app/components/AddressGeocoder/utils/useAddressAutocomplete"; import type { AddressSuggestion } from "@/app/components/AddressGeocoder/types"; @@ -269,7 +269,7 @@ export default function AddressOverlay({ /> )}
- {line1Error && } + {line1Error && } {/* Address line 2 — full width, optional */} @@ -305,7 +305,7 @@ export default function AddressOverlay({ aria-required="true" aria-invalid={cityError} /> - {cityError && } + {cityError && }
@@ -350,7 +350,7 @@ export default function AddressOverlay({ ))}
- {stateError && } + {stateError && } @@ -377,7 +377,7 @@ export default function AddressOverlay({ aria-invalid={zipError} /> {zipError && ( - + )} @@ -426,7 +426,7 @@ export default function AddressOverlay({ {countryError && ( - + )} diff --git a/app/ui/src/app/edit/components/shared/OverlayFieldError.tsx b/app/ui/src/app/edit/components/shared/FieldError.tsx similarity index 88% rename from app/ui/src/app/edit/components/shared/OverlayFieldError.tsx rename to app/ui/src/app/edit/components/shared/FieldError.tsx index d18851c5..bcc108b9 100644 --- a/app/ui/src/app/edit/components/shared/OverlayFieldError.tsx +++ b/app/ui/src/app/edit/components/shared/FieldError.tsx @@ -29,11 +29,11 @@ const WARNING_ICON = ( ); -type OverlayFieldErrorProps = { +type FieldErrorProps = { message: string; }; -export default function OverlayFieldError({ message }: OverlayFieldErrorProps) { +export default function FieldError({ message }: FieldErrorProps) { return (
{WARNING_ICON} diff --git a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx index edd8110f..b45ce4bb 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx @@ -7,7 +7,7 @@ import type { CapacityUnit, } from "@/app/edit/types/delivery"; import { useFocusTrap } from "@/app/edit/hooks/useFocusTrap"; -import OverlayFieldError from "@/app/edit/components/shared/OverlayFieldError"; +import FieldError from "@/app/edit/components/shared/FieldError"; import { OVERLAY_BACKDROP, OVERLAY_BODY, @@ -247,7 +247,7 @@ export default function VehicleDetailsOverlay({ aria-required="true" aria-invalid={nameError} /> - {nameError && } + {nameError && }
@@ -293,7 +293,7 @@ export default function VehicleDetailsOverlay({
{typeError && ( - + )} @@ -325,7 +325,7 @@ export default function VehicleDetailsOverlay({ aria-invalid={capacityError} /> {capacityError && ( - + )} @@ -374,7 +374,7 @@ export default function VehicleDetailsOverlay({ - {unitError && } + {unitError && } @@ -508,7 +508,7 @@ export default function VehicleDetailsOverlay({ {departureError && ( - + )} diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index c44a2442..69bec21c 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -357,8 +357,6 @@ export const ADDRESS_ROW_FIELD_INPUT_FILL = `${ADDRESS_ROW_FIELD_INPUT} flex-1`; export const ADDRESS_ROW_ADDR_WRAP = "relative border border-[var(--edit-stone-200)] flex h-11 items-center rounded-[6px] overflow-hidden w-full cursor-pointer"; -export const ADDRESS_ROW_ADDR_WRAP_ERROR = `${ADDRESS_ROW_ADDR_WRAP} border-[var(--edit-error-border)]`; - export const ADDRESS_ROW_ADDR_GRADIENT = "pointer-events-none absolute right-0 top-0 h-full w-[72px] bg-gradient-to-l from-[var(--edit-bg-primary)] from-[60%] to-transparent flex items-center justify-end pr-2"; @@ -373,6 +371,8 @@ export const ADDRESS_ROW_STEPPER_CONTAINER = export const ADDRESS_ROW_STEPPER_CONTAINER_NARROW = `${ADDRESS_ROW_STEPPER_CONTAINER} w-[72px]`; +export const ADDRESS_ROW_QTY_FIELD_COL = "flex flex-col gap-2"; + export const ADDRESS_ROW_STEPPER_INPUT = "flex-1 min-w-0 bg-transparent outline-none text-[16px] leading-[1.5] text-[var(--edit-text-primary)] [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"; From a095c077455506c210482c986ae8e60cdec88f55 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 22:14:25 -0700 Subject: [PATCH 014/119] refactor(edit): rename ErrorPopup to OptimizeErrorPopup for clarity --- .../shared/{ErrorPopup.tsx => OptimizeErrorPopup.tsx} | 4 ++-- app/ui/src/app/edit/page.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename app/ui/src/app/edit/components/shared/{ErrorPopup.tsx => OptimizeErrorPopup.tsx} (93%) diff --git a/app/ui/src/app/edit/components/shared/ErrorPopup.tsx b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx similarity index 93% rename from app/ui/src/app/edit/components/shared/ErrorPopup.tsx rename to app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx index c2b88694..663dd585 100644 --- a/app/ui/src/app/edit/components/shared/ErrorPopup.tsx +++ b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx @@ -15,12 +15,12 @@ import { } from "@/app/edit/formStyles"; import { useFocusTrap } from "@/app/edit/hooks/useFocusTrap"; -type ErrorPopupProps = { +type OptimizeErrorPopupProps = { message: string | null; onClose: () => void; }; -export default function ErrorPopup({ message, onClose }: ErrorPopupProps) { +export default function OptimizeErrorPopup({ message, onClose }: OptimizeErrorPopupProps) { const panelRef = useFocusTrap(!!message); const handleKeyDown = (e: React.KeyboardEvent) => { diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index 378c58d4..a356c6ae 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -10,7 +10,7 @@ import Navbar from "@/app/components/navbar/Navbar"; import MobileNavbar from "@/app/components/navbar/MobileNavbar"; import MobileSidebar from "@/app/components/sidebar/MobileSidebar"; import OptimizingModal from "@/app/edit/components/shared/OptimizingModal"; -import ErrorPopup from "@/app/edit/components/shared/ErrorPopup"; +import OptimizeErrorPopup from "@/app/edit/components/shared/OptimizeErrorPopup"; import Sidebar from "@/app/components/sidebar/Sidebar"; import SidebarEditButton from "@/app/components/sidebar/SidebarEditButton"; import SidebarResultsButton from "@/app/components/sidebar/SidebarResultsButton"; @@ -186,9 +186,9 @@ export default function Page() { /> )} - - - + + + {needsDepotAddress && ( Date: Sat, 23 May 2026 22:24:46 -0700 Subject: [PATCH 015/119] style(edit): upgrade optimizeErrorPopup from mid-fi to hi-fi design --- .../components/shared/OptimizeErrorPopup.tsx | 87 ++++++++++--------- app/ui/src/app/edit/formStyles.v2.ts | 7 ++ 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx index 663dd585..ba1baab6 100644 --- a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx +++ b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx @@ -1,18 +1,16 @@ "use client"; -/** - * Modal popup for surfacing a single error message to the user. - * Renders nothing when `message` is null. - */ - import { - ERROR_POPUP_CLOSE_ICON, - ERROR_POPUP_DISMISS_BUTTON, - MODAL_MESSAGE, - MODAL_OVERLAY, - MODAL_PANEL, - MODAL_TITLE, -} from "@/app/edit/formStyles"; + ERROR_POPUP_FOOTER, + ERROR_POPUP_MESSAGE, + OVERLAY_BACKDROP, + OVERLAY_CLOSE_BTN, + OVERLAY_HEADER, + OVERLAY_PANEL, + OVERLAY_PRIMARY_BTN, + OVERLAY_TITLE, +} from "@/app/edit/formStyles.v2"; +import styles from "@/app/edit/edit.module.css"; import { useFocusTrap } from "@/app/edit/hooks/useFocusTrap"; type OptimizeErrorPopupProps = { @@ -33,45 +31,48 @@ export default function OptimizeErrorPopup({ message, onClose }: OptimizeErrorPo return (
-
- -

- Something went wrong -

-

{message}

- + aria-hidden + > + + + + +
+

{message}

+
+ +
); diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 69bec21c..e220400b 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -759,3 +759,10 @@ export const MOBILE_BOTTOM_BAR_SECONDARY_BTN = export const MOBILE_BOTTOM_BAR_SECONDARY_LABEL = "font-['Manrope',sans-serif] font-semibold text-[16px] leading-[22px] text-[var(--edit-text-primary)] whitespace-nowrap"; + +// ── OptimizeErrorPopup ──────────────────────────────────────────────────────── + +export const ERROR_POPUP_MESSAGE = + "text-[14px] leading-5 text-[var(--edit-text-secondary)] w-full"; + +export const ERROR_POPUP_FOOTER = "flex items-center justify-end"; From 697bc76e5ae844116915e895a3fc6ecf578d3e3e Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 23:55:43 -0700 Subject: [PATCH 016/119] feat(edit): create new ui component for csv upload overlay --- .../components/address/AddressSection.tsx | 32 ++- .../components/address/CSVUploadOverlay.tsx | 222 ++++++++++++++++++ app/ui/src/app/edit/formStyles.v2.ts | 42 ++++ 3 files changed, 279 insertions(+), 17 deletions(-) create mode 100644 app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx diff --git a/app/ui/src/app/edit/components/address/AddressSection.tsx b/app/ui/src/app/edit/components/address/AddressSection.tsx index 1ef43e8c..90f051c8 100644 --- a/app/ui/src/app/edit/components/address/AddressSection.tsx +++ b/app/ui/src/app/edit/components/address/AddressSection.tsx @@ -4,11 +4,12 @@ * Addresses region: toolbar (find / add / import) and a stacked list of delivery cards for the current page. */ -import { useRef, useState } from "react"; +import { useState } from "react"; import AddressCard from "@/app/edit/components/address/AddressCard"; import ConfirmDeletionOverlay from "@/app/edit/components/shared/ConfirmDeletionOverlay"; import AddressEmptyState from "@/app/edit/components/address/AddressEmptyState"; import AddressRowHeader from "@/app/edit/components/address/AddressRowHeader"; +import CSVUploadOverlay from "@/app/edit/components/address/CSVUploadOverlay"; import type { AddressCard as AddressCardType } from "@/app/edit/types/delivery"; import { ADDRESS_EMPTY_STATE, @@ -74,7 +75,7 @@ export default function AddressSection({ outOfRegionIds, onOpenImportModal, }: AddressSectionProps) { - const fileInputRef = useRef(null); + const [isUploadOverlayOpen, setIsUploadOverlayOpen] = useState(false); const [addressToDeleteId, setAddressToDeleteId] = useState( null, ); @@ -94,19 +95,6 @@ export default function AddressSection({

- { - const file = e.target.files?.[0]; - if (file) onOpenImportModal(file); - e.target.value = ""; - }} - className="hidden" - aria-hidden="true" - /> - {/* Mobile: Search top, buttons right-aligned side-by-side (Figma 8325:7503) */}
+ {isUploadOverlayOpen && ( + setIsUploadOverlayOpen(false)} + onFileSelect={(file) => { + onOpenImportModal(file); + setIsUploadOverlayOpen(false); + }} + /> + )} + {addressToDeleteId !== null && ( void; + onFileSelect: (file: File) => void; +}; + +function formatFileSize(bytes: number): string { + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +export default function CSVUploadOverlay({ + onClose, + onFileSelect, +}: CSVUploadOverlayProps) { + const fileInputRef = useRef(null); + const [selectedFile, setSelectedFile] = useState(null); + + function handleFileChange(e: React.ChangeEvent) { + const file = e.target.files?.[0] ?? null; + setSelectedFile(file); + e.target.value = ""; + } + + function handleRemoveFile() { + setSelectedFile(null); + } + + function handleNext() { + if (selectedFile) onFileSelect(selectedFile); + } + + return ( +
+
e.stopPropagation()} + role="dialog" + aria-modal="true" + aria-labelledby="csv-upload-title" + > +
+
+
+ {/* Header */} +
+

+ Import from CSV +

+ +
+ + {/* Drop zone */} +
+
+ + +
+

+ Drag and drop CSV files here, or +

+ +
+
+ + +
+ + {/* Description */} +

+ Import delivery details from a CSV file. Maximum file size of X + MB. +

+
+ + {/* File chip — visible only when a file is selected */} + {selectedFile !== null && ( +
+
+ +

+ {selectedFile.name} +

+
+ +
+

+ {formatFileSize(selectedFile.size)} +

+ +
+
+ )} +
+ + {/* Footer */} +
+ + +
+
+
+
+ ); +} diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index e220400b..7d809f87 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -766,3 +766,45 @@ export const ERROR_POPUP_MESSAGE = "text-[14px] leading-5 text-[var(--edit-text-secondary)] w-full"; export const ERROR_POPUP_FOOTER = "flex items-center justify-end"; + +// ── CSV Upload Overlay (Figma 8102:1548 desktop / 7472:5765 mobile) ─────────── + +export const CSV_UPLOAD_OVERLAY_INNER = + "flex flex-col gap-[16px] items-end w-full"; + +export const CSV_UPLOAD_OVERLAY_CONTENT = + "flex flex-col gap-[16px] items-center w-full"; + +export const CSV_UPLOAD_OVERLAY_TOP = + "flex flex-col gap-[24px] items-start w-full"; + +export const CSV_UPLOAD_DROP_ZONE = + "bg-[var(--edit-stone-50)] border border-[var(--edit-stone-200)] border-dashed flex flex-col h-[200px] items-center justify-center overflow-clip pt-[24px] pb-[16px] rounded-[6px] w-full"; + +export const CSV_UPLOAD_DROP_ZONE_INNER = + "flex flex-col gap-[8px] items-center justify-center"; + +export const CSV_UPLOAD_DROP_ZONE_TEXT = + "font-normal leading-[1.5] text-[16px] text-[var(--edit-text-primary)] whitespace-nowrap"; + +export const CSV_UPLOAD_BROWSE_BTN = + "flex flex-col h-[36px] items-center justify-center overflow-clip px-[16px] py-[10px] rounded-[4px] font-semibold text-[14px] leading-[20px] text-[var(--edit-text-primary)] hover:bg-[var(--edit-tertiary-btn-hover)] active:bg-[var(--edit-tertiary-btn-pressed)] transition-colors cursor-pointer"; + +export const CSV_UPLOAD_DESCRIPTION = + "font-normal text-[14px] leading-[1.5] text-[var(--edit-text-secondary)] w-full"; + +export const CSV_UPLOAD_FILE_CHIP = + "bg-[var(--edit-container-active)] flex items-center justify-between p-[16px] rounded-[6px] w-full"; + +export const CSV_UPLOAD_FILE_CHIP_LEFT = "flex gap-[8px] items-center"; + +export const CSV_UPLOAD_FILE_CHIP_FILENAME = + "font-normal leading-[1.5] text-[16px] text-[var(--edit-text-primary)] whitespace-nowrap"; + +export const CSV_UPLOAD_FILE_CHIP_RIGHT = "flex gap-[8px] items-center"; + +export const CSV_UPLOAD_FILE_CHIP_SIZE = + "font-normal text-[14px] leading-[1.5] text-[var(--edit-text-secondary)] whitespace-nowrap"; + +export const CSV_UPLOAD_FILE_CHIP_REMOVE = + "flex items-center justify-center size-4 text-[var(--edit-text-primary)] hover:opacity-70 transition-opacity cursor-pointer"; From 37be1e19b070268599797360ec19e28532d5a61a Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 23:58:56 -0700 Subject: [PATCH 017/119] style(edit): increase width of csv upload overlay for desktop --- app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx | 4 ++-- app/ui/src/app/edit/formStyles.v2.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index c54689f0..64a0e686 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -3,13 +3,13 @@ import { useRef, useState } from "react"; import { OVERLAY_BACKDROP, - OVERLAY_PANEL, OVERLAY_HEADER, OVERLAY_TITLE, OVERLAY_CLOSE_BTN, OVERLAY_FOOTER, OVERLAY_CANCEL_BTN, OVERLAY_PRIMARY_BTN, + CSV_UPLOAD_OVERLAY_PANEL, CSV_UPLOAD_OVERLAY_INNER, CSV_UPLOAD_OVERLAY_CONTENT, CSV_UPLOAD_OVERLAY_TOP, @@ -60,7 +60,7 @@ export default function CSVUploadOverlay({ return (
e.stopPropagation()} role="dialog" aria-modal="true" diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 7d809f87..60e3982f 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -769,6 +769,9 @@ export const ERROR_POPUP_FOOTER = "flex items-center justify-end"; // ── CSV Upload Overlay (Figma 8102:1548 desktop / 7472:5765 mobile) ─────────── +export const CSV_UPLOAD_OVERLAY_PANEL = + "bg-[var(--edit-bg-primary)] flex flex-col gap-[14px] items-end overflow-hidden p-4 lg:p-6 rounded-[6px] w-full lg:max-w-[800px] mx-2 lg:mx-4 shadow-lg max-h-[90dvh]"; + export const CSV_UPLOAD_OVERLAY_INNER = "flex flex-col gap-[16px] items-end w-full"; From a7854369b5f5523534ba582756d58460cbfa524d Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 00:31:52 -0700 Subject: [PATCH 018/119] feat(edit): add loading state to the csvUploadOverlay --- .../components/address/AddressSection.tsx | 20 ++---- .../components/address/CSVUploadOverlay.tsx | 72 +++++++++++-------- .../edit/components/shared/SpinnerIcon.tsx | 44 ++++++++++++ app/ui/src/app/edit/formStyles.v2.ts | 11 +++ app/ui/src/app/edit/page.tsx | 16 ++++- 5 files changed, 115 insertions(+), 48 deletions(-) create mode 100644 app/ui/src/app/edit/components/shared/SpinnerIcon.tsx diff --git a/app/ui/src/app/edit/components/address/AddressSection.tsx b/app/ui/src/app/edit/components/address/AddressSection.tsx index 90f051c8..48c011ce 100644 --- a/app/ui/src/app/edit/components/address/AddressSection.tsx +++ b/app/ui/src/app/edit/components/address/AddressSection.tsx @@ -9,7 +9,6 @@ import AddressCard from "@/app/edit/components/address/AddressCard"; import ConfirmDeletionOverlay from "@/app/edit/components/shared/ConfirmDeletionOverlay"; import AddressEmptyState from "@/app/edit/components/address/AddressEmptyState"; import AddressRowHeader from "@/app/edit/components/address/AddressRowHeader"; -import CSVUploadOverlay from "@/app/edit/components/address/CSVUploadOverlay"; import type { AddressCard as AddressCardType } from "@/app/edit/types/delivery"; import { ADDRESS_EMPTY_STATE, @@ -55,7 +54,7 @@ type AddressSectionProps = { searchQuery: string; setSearchQuery: (q: string) => void; outOfRegionIds: number[]; - onOpenImportModal: (file: File) => void; + onOpenUploadOverlay: () => void; }; export default function AddressSection({ @@ -73,9 +72,8 @@ export default function AddressSection({ searchQuery, setSearchQuery, outOfRegionIds, - onOpenImportModal, + onOpenUploadOverlay, }: AddressSectionProps) { - const [isUploadOverlayOpen, setIsUploadOverlayOpen] = useState(false); const [addressToDeleteId, setAddressToDeleteId] = useState( null, ); @@ -105,7 +103,7 @@ export default function AddressSection({
- {isUploadOverlayOpen && ( - setIsUploadOverlayOpen(false)} - onFileSelect={(file) => { - onOpenImportModal(file); - setIsUploadOverlayOpen(false); - }} - /> - )} - {addressToDeleteId !== null && ( void; @@ -42,6 +43,7 @@ export default function CSVUploadOverlay({ }: CSVUploadOverlayProps) { const fileInputRef = useRef(null); const [selectedFile, setSelectedFile] = useState(null); + const [isUploading, setIsUploading] = useState(false); function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0] ?? null; @@ -54,7 +56,10 @@ export default function CSVUploadOverlay({ } function handleNext() { - if (selectedFile) onFileSelect(selectedFile); + if (selectedFile) { + setIsUploading(true); + onFileSelect(selectedFile); + } } return ( @@ -100,32 +105,38 @@ export default function CSVUploadOverlay({ {/* Drop zone */}
- + {isUploading ? ( + + ) : ( + <> + -
-

- Drag and drop CSV files here, or -

- -
+
+

+ Drag and drop CSV files here, or +

+ +
+ + )}
- Import delivery details from a CSV file. Maximum file size of X - MB. + Import delivery details from a CSV file. Maximum file size of 10 MB.

- {/* File chip — visible only when a file is selected */} - {selectedFile !== null && ( + {/* File chip — visible only when a file is selected and not uploading */} + {selectedFile !== null && !isUploading && (
Next diff --git a/app/ui/src/app/edit/components/shared/SpinnerIcon.tsx b/app/ui/src/app/edit/components/shared/SpinnerIcon.tsx new file mode 100644 index 00000000..5bd89915 --- /dev/null +++ b/app/ui/src/app/edit/components/shared/SpinnerIcon.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { + SPINNER_ICON_ARC, + SPINNER_ICON_RING, + SPINNER_ICON_WRAPPER, +} from "@/app/edit/formStyles.v2"; + +export default function SpinnerIcon() { + return ( +
+ + +
+ ); +} diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 60e3982f..1b03fc74 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -811,3 +811,14 @@ export const CSV_UPLOAD_FILE_CHIP_SIZE = export const CSV_UPLOAD_FILE_CHIP_REMOVE = "flex items-center justify-center size-4 text-[var(--edit-text-primary)] hover:opacity-70 transition-opacity cursor-pointer"; + +export const CSV_UPLOAD_UPLOADING_TEXT = + "font-normal leading-[1.5] text-[16px] text-[var(--edit-text-primary)] whitespace-nowrap"; + +// ── SpinnerIcon ─────────────────────────────────────────────────────────────── + +export const SPINNER_ICON_WRAPPER = "relative size-[33px]"; + +export const SPINNER_ICON_RING = "absolute inset-0"; + +export const SPINNER_ICON_ARC = "absolute inset-0 animate-spin"; diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index a356c6ae..ec73433a 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -30,6 +30,7 @@ import EditPageFooter from "@/app/edit/components/footer/EditPageFooter"; import MobileEditPageFooter from "@/app/edit/components/footer/MobileEditPageFooter"; import MobileBottomBar from "@/app/components/navbar/MobileBottomBar"; import { CSVImportModal } from "@/app/edit/components/address/CSVImportModal"; +import CSVUploadOverlay from "@/app/edit/components/address/CSVUploadOverlay"; import { useVehicles } from "@/app/edit/hooks/useVehicles"; import { useAddresses } from "@/app/edit/hooks/useAddresses"; import { useOptimize } from "@/app/edit/hooks/useOptimize"; @@ -82,6 +83,12 @@ export default function Page() { closeImportModal, } = useCSVImport(); + const [isUploadOverlayOpen, setIsUploadOverlayOpen] = useState(false); + + useEffect(() => { + if (isImportModalOpen || parseError) setIsUploadOverlayOpen(false); + }, [isImportModalOpen, parseError]); + useEffect(() => { let cancelled = false; @@ -175,6 +182,13 @@ export default function Page() { return (
+ {isUploadOverlayOpen && ( + setIsUploadOverlayOpen(false)} + onFileSelect={openImportModal} + /> + )} + {/* In-page import modal — stays on edit page after confirm */} {isImportModalOpen && ( setIsUploadOverlayOpen(true)} /> From 5cf1819c032f911f6de6d5de52ba887c61c70af7 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 00:33:35 -0700 Subject: [PATCH 019/119] style(edit): change location of vehicle buttons --- .../components/vehicle/VehicleSection.tsx | 42 ++++++++++--------- app/ui/src/app/edit/formStyles.v2.ts | 2 + 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx index 98f2f032..ba456bcd 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx @@ -19,9 +19,10 @@ import { VEHICLE_INFO_HEADER_ROW, VEHICLE_INFO_ROWS, VEHICLE_SECTION_BTN_GHOST, - VEHICLE_SECTION_ACTIONS, + VEHICLE_SECTION_BTN_GROUP, VEHICLE_SECTION_HEADER, VEHICLE_SECTION_HEADING, + VEHICLE_SECTION_HEADING_ROW, VEHICLE_SECTION_SUBHEADING, MOBILE_EMPTY_STATE_CONTAINER, VEHICLE_MOBILE_LIST, @@ -111,25 +112,26 @@ export default function VehicleSection({

Vehicle details

-

Manage your delivery fleet

-
- -
- - +
+

Manage your delivery fleet

+
+ + +
+
{/* Desktop: card with header + vehicle rows */} diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 1b03fc74..0e055942 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -226,6 +226,8 @@ export const VEHICLE_SECTION_ACTIONS = export const VEHICLE_SECTION_HEADING_ROW = "flex items-center justify-between"; +export const VEHICLE_SECTION_BTN_GROUP = "flex items-center gap-2"; + export const VEHICLE_SECTION_OPTIMIZE_BTN = "h-9 px-[16px] rounded-[4px] bg-[var(--edit-btn-primary)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip cursor-pointer disabled:cursor-not-allowed"; From c3d35ce440e5ae0307ce84934697ee4eb7eba18d Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 00:34:14 -0700 Subject: [PATCH 020/119] feat(edit): add file size validation for CSV import --- app/ui/src/app/edit/hooks/useCSVImport.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/ui/src/app/edit/hooks/useCSVImport.ts b/app/ui/src/app/edit/hooks/useCSVImport.ts index 8649e531..95ce3029 100644 --- a/app/ui/src/app/edit/hooks/useCSVImport.ts +++ b/app/ui/src/app/edit/hooks/useCSVImport.ts @@ -29,6 +29,13 @@ export function useCSVImport() { const [isLoading, setIsLoading] = useState(false); const openImportModal = useCallback((file: File) => { + if (file.size > 10 * 1024 * 1024) { + setParseError( + "Your file exceeds 10MB limit, please use a smaller file.", + ); + return; + } + setParseError(null); setIsLoading(true); From d36ef2fd300b48caafdc14c0f5b5698686309e15 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 00:59:50 -0700 Subject: [PATCH 021/119] fix(edit): fix location of vehicle action buttons on mobile --- app/ui/src/app/edit/formStyles.v2.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 0e055942..de88736a 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -224,9 +224,10 @@ export const VEHICLE_SECTION_BTN_GHOST = export const VEHICLE_SECTION_ACTIONS = "flex items-center justify-end gap-2 mb-4"; -export const VEHICLE_SECTION_HEADING_ROW = "flex items-center justify-between"; +export const VEHICLE_SECTION_HEADING_ROW = + "flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between"; -export const VEHICLE_SECTION_BTN_GROUP = "flex items-center gap-2"; +export const VEHICLE_SECTION_BTN_GROUP = "flex items-center gap-2 self-end lg:self-auto"; export const VEHICLE_SECTION_OPTIMIZE_BTN = "h-9 px-[16px] rounded-[4px] bg-[var(--edit-btn-primary)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip cursor-pointer disabled:cursor-not-allowed"; From 5d9ac732e860dba51d624725b06e24c192649f49 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 01:12:37 -0700 Subject: [PATCH 022/119] style(edit): upgraded optimizing modal from mid-fi to hi-fi design --- .../components/shared/OptimizingModal.tsx | 30 +++++++++---------- app/ui/src/app/edit/formStyles.v2.ts | 10 +++++++ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/app/ui/src/app/edit/components/shared/OptimizingModal.tsx b/app/ui/src/app/edit/components/shared/OptimizingModal.tsx index 1a9d5ce8..9ae6f8a9 100644 --- a/app/ui/src/app/edit/components/shared/OptimizingModal.tsx +++ b/app/ui/src/app/edit/components/shared/OptimizingModal.tsx @@ -1,14 +1,14 @@ "use client"; import { - MODAL_MESSAGE, - MODAL_OVERLAY, - MODAL_PANEL, - MODAL_TITLE, - OPTIMIZING_SPINNER, -} from "@/app/edit/formStyles"; -import { OPTIMIZING_SPINNER_WRAP } from "@/app/edit/formStyles.v2"; + OPTIMIZING_MODAL_PANEL, + OPTIMIZING_MODAL_STATUS_ROW, + OPTIMIZING_MODAL_STATUS_TEXT, + OVERLAY_BACKDROP, + OVERLAY_TITLE, +} from "@/app/edit/formStyles.v2"; import { useFocusTrap } from "@/app/edit/hooks/useFocusTrap"; +import SpinnerIcon from "@/app/edit/components/shared/SpinnerIcon"; type OptimizingModalProps = { isOpen: boolean; @@ -20,24 +20,22 @@ export default function OptimizingModal({ isOpen }: OptimizingModalProps) { if (!isOpen) return null; return ( -
+
-

- Optimizing routes… +

+ Optimizing your delivery routes

-

- This may take a few seconds. Please wait. -

-
-
diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index de88736a..510fb994 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -661,6 +661,16 @@ export const MOBILE_FOOTER_TEXT_LINE = "relative shrink-0"; export const OPTIMIZING_SPINNER_WRAP = "flex justify-center mt-2"; +// ── OptimizingModal ─────────────────────────────────────────────────────────── + +export const OPTIMIZING_MODAL_PANEL = + "bg-[var(--edit-bg-primary)] flex flex-col gap-6 overflow-hidden p-6 rounded-[6px] w-full max-w-[480px] mx-2 lg:mx-4 shadow-lg"; + +export const OPTIMIZING_MODAL_STATUS_ROW = "flex items-center gap-2 w-full"; + +export const OPTIMIZING_MODAL_STATUS_TEXT = + "font-normal text-[16px] leading-[1.5] text-[var(--edit-text-primary)] whitespace-nowrap"; + // ── Drag-Drop Overlay (Figma 8080:3134) ────────────────────────────────────── export const DRAG_DROP_OVERLAY_ROOT = From 4bfdf83b2bf47a9237ff2156cbf5c088133dc4a2 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 01:20:38 -0700 Subject: [PATCH 023/119] fix(edit): address notes field resizing bug by measuing height of notes field at every change --- .../edit/components/address/AddressCard.tsx | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/app/ui/src/app/edit/components/address/AddressCard.tsx b/app/ui/src/app/edit/components/address/AddressCard.tsx index 8c602e5c..fb506ec7 100644 --- a/app/ui/src/app/edit/components/address/AddressCard.tsx +++ b/app/ui/src/app/edit/components/address/AddressCard.tsx @@ -205,8 +205,23 @@ function AutoResizeNotesTextarea({ const textarea = textareaRef.current; if (!textarea) return; - textarea.style.height = "auto"; - textarea.style.height = `${textarea.scrollHeight}px`; + const fitHeight = () => { + textarea.style.height = "auto"; + textarea.style.height = `${textarea.scrollHeight}px`; + }; + + fitHeight(); + + const mq = window.matchMedia("(min-width: 1024px)"); + mq.addEventListener("change", fitHeight); + + const ro = new ResizeObserver(fitHeight); + ro.observe(textarea); + + return () => { + mq.removeEventListener("change", fitHeight); + ro.disconnect(); + }; }, [value]); return ( @@ -405,7 +420,7 @@ export default function AddressCard({ ) } /> - {qtyInvalid && } + {qtyInvalid && }
{/* Delivery estimation — edit */} @@ -819,7 +834,7 @@ export default function AddressCard({ ) } /> - {qtyInvalid && } + {qtyInvalid && }
From b839398c4c9f3057b190307997208c2580bb506f Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 11:26:03 -0700 Subject: [PATCH 024/119] refactor(edit): fully migrated from formStyles to formStylesv2 --- .../components/address/AddressSection.tsx | 11 +- app/ui/src/app/edit/formStyles.ts | 343 ------------------ app/ui/src/app/edit/formStyles.v2.ts | 14 +- 3 files changed, 13 insertions(+), 355 deletions(-) delete mode 100644 app/ui/src/app/edit/formStyles.ts diff --git a/app/ui/src/app/edit/components/address/AddressSection.tsx b/app/ui/src/app/edit/components/address/AddressSection.tsx index 48c011ce..1e24bd44 100644 --- a/app/ui/src/app/edit/components/address/AddressSection.tsx +++ b/app/ui/src/app/edit/components/address/AddressSection.tsx @@ -10,11 +10,6 @@ import ConfirmDeletionOverlay from "@/app/edit/components/shared/ConfirmDeletion import AddressEmptyState from "@/app/edit/components/address/AddressEmptyState"; import AddressRowHeader from "@/app/edit/components/address/AddressRowHeader"; import type { AddressCard as AddressCardType } from "@/app/edit/types/delivery"; -import { - ADDRESS_EMPTY_STATE, - ADDRESS_TOOLBAR_DESKTOP, -} from "@/app/edit/formStyles"; - import AddressSearchBar from "@/app/edit/components/address/AddressSearchBar"; import { ADDRESS_SECTION_HEADER, @@ -26,6 +21,8 @@ import { ADDRESS_LIST_CONTAINER_INNER, ADDRESS_LIST_DIVIDER, ADDRESS_SEARCH_DESKTOP_SIZE, + ADDRESS_SEARCH_NO_RESULTS, + ADDRESS_TOOLBAR_DESKTOP, ADDRESS_TOOLBAR_SPACER, MOBILE_EMPTY_STATE_CONTAINER, MOBILE_ADDR_TOOLBAR_ROOT, @@ -164,7 +161,7 @@ export default function AddressSection({ {addressesCount > 0 && (
{searchQuery.trim() !== "" && addressesOnCurrentPage.length === 0 ? ( -
No Addresses Found
+
No Addresses Found
) : ( addressesOnCurrentPage.map((a) => ( ) : searchQuery.trim() !== "" && addressesOnCurrentPage.length === 0 ? ( -
No Addresses Found
+
No Addresses Found
) : ( addressesOnCurrentPage.map((a) => ( Date: Sun, 24 May 2026 11:35:22 -0700 Subject: [PATCH 025/119] feat(edit): added support for dragging files in the csvUploadOverlay --- .../components/address/CSVUploadOverlay.tsx | 32 ++++++++++++++++++- app/ui/src/app/edit/edit.module.css | 4 +++ app/ui/src/app/edit/formStyles.v2.ts | 3 ++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index 6d3a0baf..22c00141 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -14,6 +14,7 @@ import { CSV_UPLOAD_OVERLAY_CONTENT, CSV_UPLOAD_OVERLAY_TOP, CSV_UPLOAD_DROP_ZONE, + CSV_UPLOAD_DROP_ZONE_ACTIVE, CSV_UPLOAD_DROP_ZONE_INNER, CSV_UPLOAD_DROP_ZONE_TEXT, CSV_UPLOAD_BROWSE_BTN, @@ -44,6 +45,7 @@ export default function CSVUploadOverlay({ const fileInputRef = useRef(null); const [selectedFile, setSelectedFile] = useState(null); const [isUploading, setIsUploading] = useState(false); + const [isDragOver, setIsDragOver] = useState(false); function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0] ?? null; @@ -62,6 +64,27 @@ export default function CSVUploadOverlay({ } } + function handleDragOver(e: React.DragEvent) { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(true); + } + + function handleDragLeave(e: React.DragEvent) { + e.preventDefault(); + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + setIsDragOver(false); + } + } + + function handleDrop(e: React.DragEvent) { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + const file = e.dataTransfer.files[0] ?? null; + if (file) setSelectedFile(file); + } + return (
{/* Drop zone */} -
+
{isUploading ? ( diff --git a/app/ui/src/app/edit/edit.module.css b/app/ui/src/app/edit/edit.module.css index 42704e0c..43142c25 100644 --- a/app/ui/src/app/edit/edit.module.css +++ b/app/ui/src/app/edit/edit.module.css @@ -81,6 +81,10 @@ /* ── Drag-drop overlay ──────────────────────────────────────── */ --edit-drag-overlay-bg: rgba(213, 242, 232, 0.48); + /* ── CSV drop zone drag-active state ────────────────────────── */ + --edit-drop-zone-active-border: #57ac91; + --edit-drop-zone-active-bg: #f0faf6; + /* Address illustration */ --edit-address-accent: #f97316; --edit-address-on-accent: #ffffff; diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index f23ab2d2..fc67600a 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -801,6 +801,9 @@ export const CSV_UPLOAD_OVERLAY_TOP = export const CSV_UPLOAD_DROP_ZONE = "bg-[var(--edit-stone-50)] border border-[var(--edit-stone-200)] border-dashed flex flex-col h-[200px] items-center justify-center overflow-clip pt-[24px] pb-[16px] rounded-[6px] w-full"; +export const CSV_UPLOAD_DROP_ZONE_ACTIVE = + "bg-[var(--edit-drop-zone-active-bg)] border border-[var(--edit-drop-zone-active-border)] border-dashed flex flex-col h-[200px] items-center justify-center overflow-clip pt-[24px] pb-[16px] rounded-[6px] w-full"; + export const CSV_UPLOAD_DROP_ZONE_INNER = "flex flex-col gap-[8px] items-center justify-center"; From b3c580b369c604c057273c5752d29d3ab4779ec3 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 11:48:02 -0700 Subject: [PATCH 026/119] feat(edit): prevent users from uploading non-csv files with error handling --- .../app/edit/components/address/CSVUploadOverlay.tsx | 9 ++++++++- app/ui/src/app/edit/page.tsx | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index 22c00141..c6271de3 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -31,6 +31,7 @@ import SpinnerIcon from "@/app/edit/components/shared/SpinnerIcon"; type CSVUploadOverlayProps = { onClose: () => void; onFileSelect: (file: File) => void; + onInvalidFile?: () => void; }; function formatFileSize(bytes: number): string { @@ -41,6 +42,7 @@ function formatFileSize(bytes: number): string { export default function CSVUploadOverlay({ onClose, onFileSelect, + onInvalidFile, }: CSVUploadOverlayProps) { const fileInputRef = useRef(null); const [selectedFile, setSelectedFile] = useState(null); @@ -82,7 +84,12 @@ export default function CSVUploadOverlay({ e.stopPropagation(); setIsDragOver(false); const file = e.dataTransfer.files[0] ?? null; - if (file) setSelectedFile(file); + if (!file) return; + if (file.name.toLowerCase().endsWith(".csv")) { + setSelectedFile(file); + } else { + onInvalidFile?.(); + } } return ( diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index ec73433a..3f385305 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -53,6 +53,7 @@ export default function Page() { const vehicleState = useVehicles(); const addressState = useAddresses(); const [sessionError, setSessionError] = useState(null); + const [uploadError, setUploadError] = useState(null); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const { importVehicles } = vehicleState; const { importAddresses } = addressState; @@ -186,6 +187,12 @@ export default function Page() { setIsUploadOverlayOpen(false)} onFileSelect={openImportModal} + onInvalidFile={() => { + setIsUploadOverlayOpen(false); + setUploadError( + "This file type is not accepted. Please upload a CSV file.", + ); + }} /> )} @@ -203,6 +210,10 @@ export default function Page() { + setUploadError(null)} + /> {needsDepotAddress && ( Date: Sun, 24 May 2026 13:02:05 -0700 Subject: [PATCH 027/119] feat(edit); wire up dragging files onto edit page directly with DragDropOverlay --- .../components/address/CSVUploadOverlay.tsx | 6 ++- app/ui/src/app/edit/page.tsx | 49 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index c6271de3..35e757fb 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -32,6 +32,7 @@ type CSVUploadOverlayProps = { onClose: () => void; onFileSelect: (file: File) => void; onInvalidFile?: () => void; + initialFile?: File; }; function formatFileSize(bytes: number): string { @@ -43,9 +44,12 @@ export default function CSVUploadOverlay({ onClose, onFileSelect, onInvalidFile, + initialFile, }: CSVUploadOverlayProps) { const fileInputRef = useRef(null); - const [selectedFile, setSelectedFile] = useState(null); + const [selectedFile, setSelectedFile] = useState( + initialFile ?? null, + ); const [isUploading, setIsUploading] = useState(false); const [isDragOver, setIsDragOver] = useState(false); diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index 3f385305..492c4883 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -31,6 +31,7 @@ import MobileEditPageFooter from "@/app/edit/components/footer/MobileEditPageFoo import MobileBottomBar from "@/app/components/navbar/MobileBottomBar"; import { CSVImportModal } from "@/app/edit/components/address/CSVImportModal"; import CSVUploadOverlay from "@/app/edit/components/address/CSVUploadOverlay"; +import DragDropOverlay from "@/app/edit/components/shared/DragDropOverlay"; import { useVehicles } from "@/app/edit/hooks/useVehicles"; import { useAddresses } from "@/app/edit/hooks/useAddresses"; import { useOptimize } from "@/app/edit/hooks/useOptimize"; @@ -54,6 +55,8 @@ export default function Page() { const addressState = useAddresses(); const [sessionError, setSessionError] = useState(null); const [uploadError, setUploadError] = useState(null); + const [dragCount, setDragCount] = useState(0); + const [pendingDropFile, setPendingDropFile] = useState(null); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const { importVehicles } = vehicleState; const { importAddresses } = addressState; @@ -86,6 +89,9 @@ export default function Page() { const [isUploadOverlayOpen, setIsUploadOverlayOpen] = useState(false); + const isDraggingOverPage = + dragCount > 0 && !isUploadOverlayOpen && !isImportModalOpen; + useEffect(() => { if (isImportModalOpen || parseError) setIsUploadOverlayOpen(false); }, [isImportModalOpen, parseError]); @@ -181,6 +187,39 @@ export default function Page() { [optimize], ); + useEffect(() => { + if (!isUploadOverlayOpen) setPendingDropFile(null); + }, [isUploadOverlayOpen]); + + function handlePageDragEnter(e: React.DragEvent) { + e.preventDefault(); + setDragCount((c) => c + 1); + } + + function handlePageDragLeave(e: React.DragEvent) { + e.preventDefault(); + setDragCount((c) => Math.max(0, c - 1)); + } + + function handlePageDragOver(e: React.DragEvent) { + e.preventDefault(); + } + + function handlePageDrop(e: React.DragEvent) { + e.preventDefault(); + setDragCount(0); + const file = e.dataTransfer.files[0] ?? null; + if (!file) return; + if (file.name.toLowerCase().endsWith(".csv")) { + setPendingDropFile(file); + setIsUploadOverlayOpen(true); + } else { + setUploadError( + "This file type is not accepted. Please upload a CSV file.", + ); + } + } + return (
{isUploadOverlayOpen && ( @@ -193,6 +232,7 @@ export default function Page() { "This file type is not accepted. Please upload a CSV file.", ); }} + initialFile={pendingDropFile ?? undefined} /> )} @@ -234,7 +274,14 @@ export default function Page() { -
+
+ {isDraggingOverPage && }
void optimize()} From 4eb15ac202e75e61fc5b3479478156505f494285 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:03:36 -0700 Subject: [PATCH 028/119] fix(edit): ensure DragDropOverlay covers whole body --- app/ui/src/app/edit/formStyles.v2.ts | 6 ++++-- app/ui/src/app/edit/page.tsx | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index fc67600a..0060b885 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -117,8 +117,10 @@ export const PAGE_V2_ROOT = export const PAGE_V2_BODY = "flex flex-1 lg:min-h-0 lg:overflow-hidden"; +export const PAGE_V2_MAIN_OUTER = "relative flex-1 min-w-0"; + export const PAGE_V2_MAIN = - "relative flex-1 min-w-0 bg-[var(--edit-bg-primary)] border-t-0 lg:border-t border-l-0 lg:border-l border-[var(--edit-stone-200)] rounded-none lg:rounded-tl-[12px] p-6 pb-[136px] lg:pb-6 lg:overflow-y-auto space-y-16"; + "h-full bg-[var(--edit-bg-primary)] border-t-0 lg:border-t border-l-0 lg:border-l border-[var(--edit-stone-200)] rounded-none lg:rounded-tl-[12px] p-6 pb-[136px] lg:pb-6 lg:overflow-y-auto space-y-16"; export const VEHICLE_INFO_CONTAINER = "hidden lg:flex flex-col gap-4 border border-[var(--edit-stone-200)] rounded-[8px] overflow-hidden p-4"; @@ -678,7 +680,7 @@ export const OPTIMIZING_MODAL_STATUS_TEXT = // ── Drag-Drop Overlay (Figma 8080:3134) ────────────────────────────────────── export const DRAG_DROP_OVERLAY_ROOT = - "absolute inset-0 z-50 flex items-center justify-center bg-[var(--edit-drag-overlay-bg)] border-4 border-[var(--edit-teal-600)] rounded-tl-[12px]"; + "absolute inset-0 z-50 flex items-center justify-center pointer-events-none bg-[var(--edit-drag-overlay-bg)] border-4 border-[var(--edit-teal-600)] rounded-tl-[12px]"; export const DRAG_DROP_OVERLAY_CONTENT = "flex flex-col gap-4 items-center w-[250px]"; diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index 492c4883..0bdc9677 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -17,6 +17,7 @@ import SidebarResultsButton from "@/app/components/sidebar/SidebarResultsButton" import { PAGE_V2_ROOT, PAGE_V2_BODY, + PAGE_V2_MAIN_OUTER, PAGE_V2_MAIN, ADDRESS_SECTION_WITH_PAGINATION, MANAGE_VEHICLE_GROUP, @@ -274,14 +275,15 @@ export default function Page() { -
+
{isDraggingOverPage && } +
void optimize()} @@ -305,7 +307,8 @@ export default function Page() {
-
+
+
); From 7f9a411e1d0c44ab74e99b00759fc53576020125 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:05:53 -0700 Subject: [PATCH 029/119] style(edit): improve code formatting and readability across multiple components --- .../edit/components/address/AddressCard.tsx | 4 +- .../components/address/AddressOverlay.tsx | 8 +-- .../components/address/CSVUploadOverlay.tsx | 7 ++- .../components/shared/ManageSectionHeader.tsx | 5 +- .../components/shared/OptimizeErrorPopup.tsx | 5 +- .../components/shared/OptimizingModal.tsx | 4 +- .../vehicle/VehicleDetailsOverlay.tsx | 12 ++--- .../edit/components/vehicle/VehicleRow.tsx | 4 +- .../components/vehicle/VehicleSection.tsx | 4 +- app/ui/src/app/edit/formStyles.v2.ts | 3 +- app/ui/src/app/edit/hooks/useCSVImport.ts | 4 +- app/ui/src/app/edit/page.tsx | 53 ++++++++++--------- 12 files changed, 61 insertions(+), 52 deletions(-) diff --git a/app/ui/src/app/edit/components/address/AddressCard.tsx b/app/ui/src/app/edit/components/address/AddressCard.tsx index fb506ec7..32c047e9 100644 --- a/app/ui/src/app/edit/components/address/AddressCard.tsx +++ b/app/ui/src/app/edit/components/address/AddressCard.tsx @@ -404,7 +404,9 @@ export default function AddressCard({ min={1} max={1_000_000} ariaLabel="Delivery quantity" - onChange={(v) => updateAddress(a.id, "deliveryQuantity", v)} + onChange={(v) => + updateAddress(a.id, "deliveryQuantity", v) + } onIncrement={() => updateAddress( a.id, diff --git a/app/ui/src/app/edit/components/address/AddressOverlay.tsx b/app/ui/src/app/edit/components/address/AddressOverlay.tsx index d88d9c51..0f769bc5 100644 --- a/app/ui/src/app/edit/components/address/AddressOverlay.tsx +++ b/app/ui/src/app/edit/components/address/AddressOverlay.tsx @@ -376,9 +376,7 @@ export default function AddressOverlay({ aria-required="true" aria-invalid={zipError} /> - {zipError && ( - - )} + {zipError && }
@@ -425,9 +423,7 @@ export default function AddressOverlay({ ))}
- {countryError && ( - - )} + {countryError && }
diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index 35e757fb..be3164e3 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -139,7 +139,9 @@ export default function CSVUploadOverlay({ {/* Drop zone */}
- Import delivery details from a CSV file. Maximum file size of 10 MB. + Import delivery details from a CSV file. Maximum file size of 10 + MB.

diff --git a/app/ui/src/app/edit/components/shared/ManageSectionHeader.tsx b/app/ui/src/app/edit/components/shared/ManageSectionHeader.tsx index cdea3a5e..45cc8d81 100644 --- a/app/ui/src/app/edit/components/shared/ManageSectionHeader.tsx +++ b/app/ui/src/app/edit/components/shared/ManageSectionHeader.tsx @@ -12,7 +12,10 @@ type Props = { isOptimizing: boolean; }; -export default function ManageSectionHeader({ onOptimize, isOptimizing }: Props) { +export default function ManageSectionHeader({ + onOptimize, + isOptimizing, +}: Props) { return (

Manage

diff --git a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx index ba1baab6..e146641e 100644 --- a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx +++ b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx @@ -18,7 +18,10 @@ type OptimizeErrorPopupProps = { onClose: () => void; }; -export default function OptimizeErrorPopup({ message, onClose }: OptimizeErrorPopupProps) { +export default function OptimizeErrorPopup({ + message, + onClose, +}: OptimizeErrorPopupProps) { const panelRef = useFocusTrap(!!message); const handleKeyDown = (e: React.KeyboardEvent) => { diff --git a/app/ui/src/app/edit/components/shared/OptimizingModal.tsx b/app/ui/src/app/edit/components/shared/OptimizingModal.tsx index 9ae6f8a9..3540909a 100644 --- a/app/ui/src/app/edit/components/shared/OptimizingModal.tsx +++ b/app/ui/src/app/edit/components/shared/OptimizingModal.tsx @@ -35,7 +35,9 @@ export default function OptimizingModal({ isOpen }: OptimizingModalProps) {
-

Creating your delivery routes

+

+ Creating your delivery routes +

diff --git a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx index b45ce4bb..774d218d 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx @@ -292,9 +292,7 @@ export default function VehicleDetailsOverlay({
- {typeError && ( - - )} + {typeError && }
@@ -324,9 +322,7 @@ export default function VehicleDetailsOverlay({ aria-required="true" aria-invalid={capacityError} /> - {capacityError && ( - - )} + {capacityError && }
@@ -507,9 +503,7 @@ export default function VehicleDetailsOverlay({
- {departureError && ( - - )} + {departureError && } diff --git a/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx b/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx index 2bed40d4..2babb648 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx @@ -138,7 +138,9 @@ export default function VehicleRow({ onClick={() => updateVehicle(v.id, "available", true)} aria-pressed={v.available} className={ - v.available ? STATUS_TOGGLE_BTN_ACTIVE : STATUS_TOGGLE_BTN_INACTIVE + v.available + ? STATUS_TOGGLE_BTN_ACTIVE + : STATUS_TOGGLE_BTN_INACTIVE } > Available diff --git a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx index ba456bcd..43296d23 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx @@ -113,7 +113,9 @@ export default function VehicleSection({

Vehicle details

-

Manage your delivery fleet

+

+ Manage your delivery fleet +

From 0b4c6807b6f0a2aea5ba28727554ca4bea8f5690 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:29:32 -0700 Subject: [PATCH 030/119] style(edit); change quantity invalid error message to a minimalistic red border --- .../edit/components/address/AddressCard.tsx | 65 ++++++++++--------- app/ui/src/app/edit/formStyles.v2.ts | 3 +- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/app/ui/src/app/edit/components/address/AddressCard.tsx b/app/ui/src/app/edit/components/address/AddressCard.tsx index 32c047e9..a78c06aa 100644 --- a/app/ui/src/app/edit/components/address/AddressCard.tsx +++ b/app/ui/src/app/edit/components/address/AddressCard.tsx @@ -19,6 +19,7 @@ import { ADDRESS_ROW_ADDR_TRIGGER_TEXT, ADDRESS_ROW_ADDR_TRIGGER_PLACEHOLDER, ADDRESS_ROW_STEPPER_CONTAINER_NARROW, + ADDRESS_ROW_STEPPER_CONTAINER_NARROW_ERROR, ADDRESS_ROW_STEPPER_INPUT, ADDRESS_ROW_STEPPER_CONTROLS, ADDRESS_ROW_STEPPER_BUTTON, @@ -68,7 +69,6 @@ import { ADDRESS_CARD_MOBILE_ROOT, MOBILE_ADDR_EXPANDED_PANEL, MOBILE_ADDR_LOCKED_NOTES_CLAMP, - ADDRESS_ROW_QTY_FIELD_COL, } from "@/app/edit/formStyles.v2"; import FieldError from "@/app/edit/components/shared/FieldError"; import { @@ -118,6 +118,7 @@ function StepperInput({ min = 0, max, ariaLabel, + invalid = false, onIncrement, onDecrement, onChange, @@ -126,12 +127,19 @@ function StepperInput({ min?: number; max?: number; ariaLabel: string; + invalid?: boolean; onIncrement: () => void; onDecrement: () => void; onChange: (v: number) => void; }) { return ( -
+
@@ -398,32 +407,30 @@ export default function AddressCard({
{/* Quantity — edit */} -
- - updateAddress(a.id, "deliveryQuantity", v) - } - onIncrement={() => - updateAddress( - a.id, - "deliveryQuantity", - Math.min(1_000_000, (a.deliveryQuantity || 0) + 1), - ) - } - onDecrement={() => - updateAddress( - a.id, - "deliveryQuantity", - Math.max(1, (a.deliveryQuantity || 1) - 1), - ) - } - /> - {qtyInvalid && } -
+ + updateAddress(a.id, "deliveryQuantity", v) + } + onIncrement={() => + updateAddress( + a.id, + "deliveryQuantity", + Math.min(1_000_000, (a.deliveryQuantity || 0) + 1), + ) + } + onDecrement={() => + updateAddress( + a.id, + "deliveryQuantity", + Math.max(1, (a.deliveryQuantity || 1) - 1), + ) + } + /> {/* Delivery estimation — edit */}
@@ -818,6 +825,7 @@ export default function AddressCard({ min={1} max={1_000_000} ariaLabel="Delivery quantity" + invalid={qtyInvalid} onChange={(v) => updateAddress(a.id, "deliveryQuantity", v) } @@ -836,7 +844,6 @@ export default function AddressCard({ ) } /> - {qtyInvalid && }
diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 351388c8..bc8ed8bc 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -381,7 +381,8 @@ export const ADDRESS_ROW_STEPPER_CONTAINER = export const ADDRESS_ROW_STEPPER_CONTAINER_NARROW = `${ADDRESS_ROW_STEPPER_CONTAINER} w-[72px]`; -export const ADDRESS_ROW_QTY_FIELD_COL = "flex flex-col gap-2"; +export const ADDRESS_ROW_STEPPER_CONTAINER_NARROW_ERROR = + "border border-[var(--edit-error-border)] flex h-11 items-center justify-between px-2 py-[10px] rounded-[6px] shrink-0 w-[72px]"; export const ADDRESS_ROW_STEPPER_INPUT = "flex-1 min-w-0 bg-transparent outline-none text-[16px] leading-[1.5] text-[var(--edit-text-primary)] [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"; From 661a7ac8547b427333ed48c659c89a540023b574 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:30:56 -0700 Subject: [PATCH 031/119] refactor(edit); do a try/catch when setting storage to catch imports that are too large to save --- .../app/edit/components/address/CSVImportModal.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/edit/components/address/CSVImportModal.tsx b/app/ui/src/app/edit/components/address/CSVImportModal.tsx index 8fd8d0ad..57397717 100644 --- a/app/ui/src/app/edit/components/address/CSVImportModal.tsx +++ b/app/ui/src/app/edit/components/address/CSVImportModal.tsx @@ -698,8 +698,16 @@ export function CSVImportModal({ // Store the fully-built AddressCard[] directly — no re-parsing needed. // edit/page.tsx reads "importedCards" on mount and calls importAddresses() // directly, bypassing parseAddressUpload entirely. - sessionStorage.setItem("importedCards", JSON.stringify(cards)); - router.push("/edit"); + try { + sessionStorage.setItem("importedCards", JSON.stringify(cards)); + router.push("/edit"); + } catch (e) { + if (e instanceof DOMException && e.name === "QuotaExceededError") { + alert("Import is too large to save. Please reduce the number of selected rows."); + } else { + throw e; + } + } } else if (importAddresses) { importAddresses(cards); onClose(); From 6a4cd403063d72d82f6810e947eb6021f7e64e9f Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:31:33 -0700 Subject: [PATCH 032/119] refactor(edit): check for invalid files on handleFileChange --- .../src/app/edit/components/address/CSVUploadOverlay.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index be3164e3..fd6ea440 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -55,7 +55,12 @@ export default function CSVUploadOverlay({ function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0] ?? null; - setSelectedFile(file); + if(!file) return; + if(file.name.toLowerCase().endsWith(".csv")) { + setSelectedFile(file); + } else { + onInvalidFile?.(); + } e.target.value = ""; } From 9021574f488ee92a52c1ca943d7b13b999cb2d57 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:32:35 -0700 Subject: [PATCH 033/119] fix(edit): be explicit with aria-hidden --- app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx index e146641e..3ea57cf2 100644 --- a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx +++ b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx @@ -59,7 +59,7 @@ export default function OptimizeErrorPopup({ stroke="currentColor" strokeWidth="2" strokeLinecap="round" - aria-hidden + aria-hidden="true" > From 076209d1c228b903fb20f51ef11bef3cd3676f50 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:33:51 -0700 Subject: [PATCH 034/119] style(edit): ran prettier for cleaner formatting --- app/ui/src/app/edit/components/address/AddressCard.tsx | 4 +--- app/ui/src/app/edit/components/address/CSVImportModal.tsx | 4 +++- app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/ui/src/app/edit/components/address/AddressCard.tsx b/app/ui/src/app/edit/components/address/AddressCard.tsx index a78c06aa..d6ad393a 100644 --- a/app/ui/src/app/edit/components/address/AddressCard.tsx +++ b/app/ui/src/app/edit/components/address/AddressCard.tsx @@ -413,9 +413,7 @@ export default function AddressCard({ max={1_000_000} ariaLabel="Delivery quantity" invalid={qtyInvalid} - onChange={(v) => - updateAddress(a.id, "deliveryQuantity", v) - } + onChange={(v) => updateAddress(a.id, "deliveryQuantity", v)} onIncrement={() => updateAddress( a.id, diff --git a/app/ui/src/app/edit/components/address/CSVImportModal.tsx b/app/ui/src/app/edit/components/address/CSVImportModal.tsx index 57397717..51a27789 100644 --- a/app/ui/src/app/edit/components/address/CSVImportModal.tsx +++ b/app/ui/src/app/edit/components/address/CSVImportModal.tsx @@ -703,7 +703,9 @@ export function CSVImportModal({ router.push("/edit"); } catch (e) { if (e instanceof DOMException && e.name === "QuotaExceededError") { - alert("Import is too large to save. Please reduce the number of selected rows."); + alert( + "Import is too large to save. Please reduce the number of selected rows.", + ); } else { throw e; } diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index fd6ea440..3bec2733 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -55,8 +55,8 @@ export default function CSVUploadOverlay({ function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0] ?? null; - if(!file) return; - if(file.name.toLowerCase().endsWith(".csv")) { + if (!file) return; + if (file.name.toLowerCase().endsWith(".csv")) { setSelectedFile(file); } else { onInvalidFile?.(); From a1a131fb0e909ed54f8dd3b86363337053620024 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:45:57 -0700 Subject: [PATCH 035/119] fix(edit): address incorrect aria-disabled string issue --- app/ui/src/app/components/sidebar/MobileSidebar.tsx | 2 +- app/ui/src/app/components/sidebar/SidebarResultsButton.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/components/sidebar/MobileSidebar.tsx b/app/ui/src/app/components/sidebar/MobileSidebar.tsx index 0353eb0d..95f2ed03 100644 --- a/app/ui/src/app/components/sidebar/MobileSidebar.tsx +++ b/app/ui/src/app/components/sidebar/MobileSidebar.tsx @@ -134,7 +134,7 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) { diff --git a/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx b/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx index b3fb88cf..dbc3df54 100644 --- a/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx +++ b/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx @@ -25,7 +25,7 @@ export default function SidebarResultsButton() { {" "} From 50a8248d591d1f87d73d19e11a67d4cc499dee30 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 14:04:09 -0700 Subject: [PATCH 036/119] fix(edit): remove alert and use proper error handling in CSVImportModal --- .../app/edit/components/address/CSVImportModal.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/edit/components/address/CSVImportModal.tsx b/app/ui/src/app/edit/components/address/CSVImportModal.tsx index 51a27789..427faff9 100644 --- a/app/ui/src/app/edit/components/address/CSVImportModal.tsx +++ b/app/ui/src/app/edit/components/address/CSVImportModal.tsx @@ -19,6 +19,7 @@ import { useState, useCallback, useMemo } from "react"; import { useRouter } from "next/navigation"; import type { AddressCard } from "@/app/edit/types/delivery"; +import OptimizeErrorPopup from "@/app/edit/components/shared/OptimizeErrorPopup"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -649,6 +650,7 @@ export function CSVImportModal({ }: CSVImportModalProps) { const router = useRouter(); const [step, setStep] = useState<1 | 2>(1); + const [errorMessage, setErrorMessage] = useState(null); const headers = useMemo(() => csvData[0] ?? [], [csvData]); const dataRows = useMemo( @@ -703,11 +705,12 @@ export function CSVImportModal({ router.push("/edit"); } catch (e) { if (e instanceof DOMException && e.name === "QuotaExceededError") { - alert( + setErrorMessage( "Import is too large to save. Please reduce the number of selected rows.", ); } else { - throw e; + console.error(e); + setErrorMessage("Something went wrong"); } } } else if (importAddresses) { @@ -728,6 +731,10 @@ export function CSVImportModal({ return ( <> + setErrorMessage(null)} + /> {step === 1 ? ( Date: Sun, 24 May 2026 14:07:46 -0700 Subject: [PATCH 037/119] refactor(edit): rename OptimizeErrorPopup to ErrorOverlay and update related components --- .../edit/components/address/CSVImportModal.tsx | 4 ++-- ...OptimizeErrorPopup.tsx => ErrorOverlay.tsx} | 18 +++++++++--------- app/ui/src/app/edit/formStyles.v2.ts | 6 +++--- app/ui/src/app/edit/page.tsx | 10 +++++----- 4 files changed, 19 insertions(+), 19 deletions(-) rename app/ui/src/app/edit/components/shared/{OptimizeErrorPopup.tsx => ErrorOverlay.tsx} (82%) diff --git a/app/ui/src/app/edit/components/address/CSVImportModal.tsx b/app/ui/src/app/edit/components/address/CSVImportModal.tsx index 427faff9..438d0a0d 100644 --- a/app/ui/src/app/edit/components/address/CSVImportModal.tsx +++ b/app/ui/src/app/edit/components/address/CSVImportModal.tsx @@ -19,7 +19,7 @@ import { useState, useCallback, useMemo } from "react"; import { useRouter } from "next/navigation"; import type { AddressCard } from "@/app/edit/types/delivery"; -import OptimizeErrorPopup from "@/app/edit/components/shared/OptimizeErrorPopup"; +import ErrorOverlay from "@/app/edit/components/shared/ErrorOverlay"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -731,7 +731,7 @@ export function CSVImportModal({ return ( <> - setErrorMessage(null)} /> diff --git a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx b/app/ui/src/app/edit/components/shared/ErrorOverlay.tsx similarity index 82% rename from app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx rename to app/ui/src/app/edit/components/shared/ErrorOverlay.tsx index 3ea57cf2..e9705433 100644 --- a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx +++ b/app/ui/src/app/edit/components/shared/ErrorOverlay.tsx @@ -1,8 +1,8 @@ "use client"; import { - ERROR_POPUP_FOOTER, - ERROR_POPUP_MESSAGE, + ERROR_OVERLAY_FOOTER, + ERROR_OVERLAY_MESSAGE, OVERLAY_BACKDROP, OVERLAY_CLOSE_BTN, OVERLAY_HEADER, @@ -13,15 +13,15 @@ import { import styles from "@/app/edit/edit.module.css"; import { useFocusTrap } from "@/app/edit/hooks/useFocusTrap"; -type OptimizeErrorPopupProps = { +type ErrorOverlayProps = { message: string | null; onClose: () => void; }; -export default function OptimizeErrorPopup({ +export default function ErrorOverlay({ message, onClose, -}: OptimizeErrorPopupProps) { +}: ErrorOverlayProps) { const panelRef = useFocusTrap(!!message); const handleKeyDown = (e: React.KeyboardEvent) => { @@ -37,12 +37,12 @@ export default function OptimizeErrorPopup({ className={OVERLAY_BACKDROP} role="dialog" aria-modal="true" - aria-labelledby="error-popup-title" + aria-labelledby="error-overlay-title" onKeyDown={handleKeyDown} >
-

+

Something went wrong

-

{message}

-
+

{message}

+
-

Tap to toggle

From 91a10fcfbdf6cd1c1c00e2c110f46371c5051a3a Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 14:41:26 -0700 Subject: [PATCH 043/119] refactor(edit): break one css component into two components for clarity --- app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx | 3 ++- app/ui/src/app/edit/formStyles.v2.ts | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index 889b43ab..85b4f53b 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -16,6 +16,7 @@ import { CSV_UPLOAD_DROP_ZONE, CSV_UPLOAD_DROP_ZONE_ACTIVE, CSV_UPLOAD_DROP_ZONE_INNER, + CSV_UPLOAD_DROP_ZONE_PROMPT, CSV_UPLOAD_DROP_ZONE_TEXT, CSV_UPLOAD_BROWSE_BTN, CSV_UPLOAD_DESCRIPTION, @@ -176,7 +177,7 @@ export default function CSVUploadOverlay({ /> -
+

Drag and drop CSV files here, or

diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 3b8e2a7b..541f82d3 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -811,6 +811,9 @@ export const CSV_UPLOAD_DROP_ZONE_ACTIVE = export const CSV_UPLOAD_DROP_ZONE_INNER = "flex flex-col gap-[8px] items-center justify-center"; +export const CSV_UPLOAD_DROP_ZONE_PROMPT = + "flex flex-col gap-[8px] items-center justify-center"; + export const CSV_UPLOAD_DROP_ZONE_TEXT = "font-normal leading-[1.5] text-[16px] text-[var(--edit-text-primary)] whitespace-nowrap"; From 1a90ed42b990b3189dfefd85a6415013eef754e3 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 14:44:35 -0700 Subject: [PATCH 044/119] chore(edit): remove unused css components --- app/ui/src/app/edit/formStyles.v2.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 541f82d3..deda8ad0 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -221,9 +221,6 @@ export const VEHICLE_MOBILE_LOCKED_DEPARTURE = export const VEHICLE_SECTION_BTN_GHOST = "h-9 px-4 rounded-[80px] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-tertiary-btn-hover)] active:bg-[var(--edit-tertiary-btn-pressed)] transition-colors cursor-pointer"; -export const VEHICLE_SECTION_ACTIONS = - "flex items-center justify-end gap-2 mb-4"; - export const VEHICLE_SECTION_HEADING_ROW = "flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between"; @@ -667,8 +664,6 @@ export const MOBILE_FOOTER_TEXT_WRAPPER = export const MOBILE_FOOTER_TEXT_LINE = "relative shrink-0"; -export const OPTIMIZING_SPINNER_WRAP = "flex justify-center mt-2"; - // ── OptimizingModal ─────────────────────────────────────────────────────────── export const OPTIMIZING_MODAL_PANEL = @@ -839,9 +834,6 @@ export const CSV_UPLOAD_FILE_CHIP_SIZE = export const CSV_UPLOAD_FILE_CHIP_REMOVE = "flex items-center justify-center size-4 text-[var(--edit-text-primary)] hover:opacity-70 transition-opacity cursor-pointer"; -export const CSV_UPLOAD_UPLOADING_TEXT = - "font-normal leading-[1.5] text-[16px] text-[var(--edit-text-primary)] whitespace-nowrap"; - // ── SpinnerIcon ─────────────────────────────────────────────────────────────── export const SPINNER_ICON_WRAPPER = "relative size-[33px]"; From 811da65bc7a57bf92d94de7854d5bb9b314abbb2 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 14:48:21 -0700 Subject: [PATCH 045/119] chore(edit): remove another unused component --- .../src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx | 1 - app/ui/src/app/edit/formStyles.v2.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx index ee5a5d62..d3cada58 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx @@ -36,7 +36,6 @@ import { OVERLAY_SELECT_ICON, OVERLAY_SELECT_WRAPPER, OVERLAY_SELECT_WRAPPER_ERROR, - OVERLAY_STATUS_HINT, OVERLAY_STATUS_ROW, STATUS_TOGGLE_WRAPPER, STATUS_TOGGLE_BTN_ACTIVE, diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index deda8ad0..68d142f9 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -517,9 +517,6 @@ export const OVERLAY_STATUS_BADGE_TEXT_AVAILABLE = export const OVERLAY_STATUS_BADGE_TEXT_IN_USE = "font-semibold text-[16px] leading-[22px] text-[var(--edit-text-secondary)] whitespace-nowrap"; -export const OVERLAY_STATUS_HINT = - "text-[12px] leading-normal text-[var(--edit-text-secondary)]"; - export const OVERLAY_STATUS_ROW = "flex gap-2 items-center"; export const OVERLAY_DEPARTURE_WRAPPER = From 1ad42c32b76b2299e4747091c02d75f9d093dae1 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 14:54:47 -0700 Subject: [PATCH 046/119] style(edit): updated departureTime field design to new hi-fi design --- .../vehicle/VehicleDetailsOverlay.tsx | 99 ++++++++++--------- app/ui/src/app/edit/formStyles.v2.ts | 12 ++- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx index d3cada58..bd421a42 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx @@ -13,6 +13,7 @@ import { OVERLAY_BODY, OVERLAY_CANCEL_BTN, OVERLAY_CLOSE_BTN, + OVERLAY_DEPARTURE_ROW, OVERLAY_DEPARTURE_WRAPPER, OVERLAY_DEPARTURE_WRAPPER_ERROR, OVERLAY_TIME_COLON, @@ -421,54 +422,56 @@ export default function VehicleDetailsOverlay({ * -
-
- { - const val = e.target.value - .replace(/\D/g, "") - .slice(0, 2); - setHours(val); - if (val.length === 2) minutesRef.current?.focus(); - }} - placeholder="HH" - maxLength={2} - inputMode="numeric" - className={OVERLAY_TIME_SEGMENT_INPUT} - aria-required="true" - aria-label="Departure hours" - aria-invalid={departureError} - /> - : - { - const val = e.target.value - .replace(/\D/g, "") - .slice(0, 2); - setMinutes(val); - }} - onKeyDown={(e) => { - if (e.key === "Backspace" && minutes === "") - hoursRef.current?.focus(); - }} - placeholder="MM" - maxLength={2} - inputMode="numeric" - className={OVERLAY_TIME_SEGMENT_INPUT} - aria-label="Departure minutes" - aria-invalid={departureError} - /> +
+
+
+ { + const val = e.target.value + .replace(/\D/g, "") + .slice(0, 2); + setHours(val); + if (val.length === 2) minutesRef.current?.focus(); + }} + placeholder="HH" + maxLength={2} + inputMode="numeric" + className={OVERLAY_TIME_SEGMENT_INPUT} + aria-required="true" + aria-label="Departure hours" + aria-invalid={departureError} + /> + : + { + const val = e.target.value + .replace(/\D/g, "") + .slice(0, 2); + setMinutes(val); + }} + onKeyDown={(e) => { + if (e.key === "Backspace" && minutes === "") + hoursRef.current?.focus(); + }} + placeholder="MM" + maxLength={2} + inputMode="numeric" + className={OVERLAY_TIME_SEGMENT_INPUT} + aria-label="Departure minutes" + aria-invalid={departureError} + /> +
Date: Sat, 23 May 2026 12:24:37 -0700 Subject: [PATCH 047/119] style(edit): simplify navbar as to match the new figma design --- .gitignore | 1 + .../layout/navbar/MobileBottomBar.tsx | 31 +--------- .../edit/components/layout/navbar/Navbar.tsx | 56 +++---------------- app/ui/src/app/edit/formStyles.v2.ts | 3 + app/ui/src/app/edit/page.tsx | 23 +------- 5 files changed, 17 insertions(+), 97 deletions(-) diff --git a/.gitignore b/.gitignore index 10e82939..1a6a98ca 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,4 @@ services/vroom/data/ # Claude Claude.md +.claude/ diff --git a/app/ui/src/app/edit/components/layout/navbar/MobileBottomBar.tsx b/app/ui/src/app/edit/components/layout/navbar/MobileBottomBar.tsx index fbeb4320..a51e18f2 100644 --- a/app/ui/src/app/edit/components/layout/navbar/MobileBottomBar.tsx +++ b/app/ui/src/app/edit/components/layout/navbar/MobileBottomBar.tsx @@ -1,41 +1,19 @@ import { MOBILE_BOTTOM_BAR_ROOT, MOBILE_BOTTOM_BAR_INNER, - MOBILE_BOTTOM_BAR_OPTIMIZE_BTN, - MOBILE_BOTTOM_BAR_OPTIMIZE_LABEL, MOBILE_BOTTOM_BAR_ACTIONS_ROW, MOBILE_BOTTOM_BAR_SECONDARY_BTN, MOBILE_BOTTOM_BAR_SECONDARY_LABEL, } from "@/app/edit/formStyles.v2"; -import styles from "@/app/edit/edit.module.css"; type Props = { - onOptimize: () => void; onSave: () => void; - onExport: () => void; - isOptimizing?: boolean; }; -export default function MobileBottomBar({ - onOptimize, - onSave, - onExport, - isOptimizing, -}: Props) { +export default function MobileBottomBar({ onSave }: Props) { return (
-
-
diff --git a/app/ui/src/app/edit/components/layout/navbar/Navbar.tsx b/app/ui/src/app/edit/components/layout/navbar/Navbar.tsx index f58c8cbb..396659ac 100644 --- a/app/ui/src/app/edit/components/layout/navbar/Navbar.tsx +++ b/app/ui/src/app/edit/components/layout/navbar/Navbar.tsx @@ -1,60 +1,22 @@ "use client"; import { - NAVBAR_V2_ACTIONS, - NAVBAR_V2_BTN_FILLED, - NAVBAR_V2_BTN_OUTLINE, + NAVBAR_V2_BTN_SAVE, NAVBAR_V2_LOGO, NAVBAR_V2_ROOT, } from "@/app/edit/formStyles.v2"; -import styles from "@/app/edit/edit.module.css"; -import ErrorPopup from "@/app/edit/components/shared/ErrorPopup"; type NavbarProps = { - onImportSession: () => void; - onExportSession: () => void; - onOptimize: () => void; - isOptimizing: boolean; - error: string | null; - onClearError: () => void; + onSave: () => void; }; -export default function Navbar({ - onImportSession, - onExportSession, - onOptimize, - isOptimizing, - error, - onClearError, -}: NavbarProps) { +export default function Navbar({ onSave }: NavbarProps) { return ( - <> - -
- DELIVERY OPTIMIZER -
- - - -
-
- +
+ Delivery Optimizer + +
); } diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index ce396ea6..42596111 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -105,6 +105,9 @@ export const NAVBAR_V2_LOGO = export const NAVBAR_V2_ACTIONS = "flex items-center gap-2"; +export const NAVBAR_V2_BTN_SAVE = + "h-9 px-[16px] rounded-[5px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; + export const NAVBAR_V2_BTN_OUTLINE = "h-9 px-4 rounded-[80px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"; diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index f83155c0..c05e9b63 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -60,7 +60,7 @@ export default function Page() { optimize, isOptimizing, optimizeError, - clearOptimizeError, + clearOptimizeError, needsDepotAddress, dismissDepotAddressPrompt, geocodeFailedAddressIds, @@ -204,26 +204,9 @@ export default function Page() { isOpen={isMobileMenuOpen} onClose={() => setIsMobileMenuOpen(false)} /> - void optimize()} - onSave={handleExportSession} - onExport={handleExportSession} - isOptimizing={isOptimizing} - /> + {}} /> setIsMobileMenuOpen(true)} /> - void optimize()} - isOptimizing={isOptimizing} - error={sessionError ?? optimizeError ?? csvError ?? parseError} - onClearError={() => { - clearSessionError(); - clearOptimizeError(); - clearCsvError(); - closeImportModal(); - }} - /> + {}} />
From 76cad4bc32948c792d7fb991d6372e7174733cc7 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 12:31:38 -0700 Subject: [PATCH 048/119] fix(edit): update button label in VehicleDetailsOverlay from 'Done' to 'Confirm' for improved clarity --- .../src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx index 20e389ec..c746a5a4 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx @@ -520,7 +520,7 @@ export default function VehicleDetailsOverlay({ onClick={handleSave} className={`${OVERLAY_PRIMARY_BTN} ${styles.primaryBtnOverlay}`} > - Done + Confirm
From 26aa77223f5c38a4b7ee4f0884fc8f5333113a1a Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 12:43:28 -0700 Subject: [PATCH 049/119] style(edit): change the location of the optimize button --- .../components/vehicle/VehicleSection.tsx | 20 ++++++++++++++++++- app/ui/src/app/edit/formStyles.v2.ts | 5 +++++ app/ui/src/app/edit/page.tsx | 4 ++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx index 98f2f032..2df10d74 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx @@ -5,6 +5,7 @@ */ import { useState } from "react"; +import styles from "@/app/edit/edit.module.css"; import VehicleRow from "@/app/edit/components/vehicle/VehicleRow"; import VehicleEmptyState from "@/app/edit/components/vehicle/VehicleEmptyState"; import VehicleDetailsOverlay from "@/app/edit/components/vehicle/VehicleDetailsOverlay"; @@ -22,6 +23,8 @@ import { VEHICLE_SECTION_ACTIONS, VEHICLE_SECTION_HEADER, VEHICLE_SECTION_HEADING, + VEHICLE_SECTION_HEADING_ROW, + VEHICLE_SECTION_OPTIMIZE_BTN, VEHICLE_SECTION_SUBHEADING, MOBILE_EMPTY_STATE_CONTAINER, VEHICLE_MOBILE_LIST, @@ -66,6 +69,8 @@ type VehicleSectionProps = { activeVehicleIsValid: boolean; geocodeFailedVehicleIds: number[]; outOfRegionVehicleIds: number[]; + onOptimize: () => void; + isOptimizing: boolean; }; export default function VehicleSection({ @@ -79,6 +84,8 @@ export default function VehicleSection({ activeVehicleIsValid, geocodeFailedVehicleIds, outOfRegionVehicleIds, + onOptimize, + isOptimizing, }: VehicleSectionProps) { const [isAddOverlayOpen, setIsAddOverlayOpen] = useState(false); const [editingVehicle, setEditingVehicle] = useState( @@ -110,7 +117,18 @@ export default function VehicleSection({ return (
-

Vehicle details

+
+

Vehicle details

+ +

Manage your delivery fleet

diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 42596111..72280f84 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -211,6 +211,11 @@ export const VEHICLE_SECTION_BTN_GHOST = export const VEHICLE_SECTION_ACTIONS = "flex items-center justify-end gap-2 mb-4"; +export const VEHICLE_SECTION_HEADING_ROW = "flex items-center justify-between"; + +export const VEHICLE_SECTION_OPTIMIZE_BTN = + "h-9 px-[16px] rounded-[4px] bg-[var(--edit-btn-primary)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip cursor-pointer disabled:cursor-not-allowed"; + export const VEHICLE_SECTION_HEADER = "flex flex-col gap-2 mb-4"; export const VEHICLE_SECTION_HEADING = diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index c05e9b63..2d587393 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -10,6 +10,7 @@ import Navbar from "@/app/edit/components/layout/navbar/Navbar"; import MobileNavbar from "@/app/edit/components/layout/navbar/MobileNavbar"; import MobileSidebar from "@/app/edit/components/layout/sidebar/MobileSidebar"; import OptimizingModal from "@/app/edit/components/shared/OptimizingModal"; +import ErrorPopup from "@/app/edit/components/shared/ErrorPopup"; import Sidebar from "@/app/edit/components/layout/sidebar/Sidebar"; import SidebarEditButton from "@/app/edit/components/layout/sidebar/SidebarEditButton"; import SidebarResultsButton from "@/app/edit/components/layout/sidebar/SidebarResultsButton"; @@ -192,6 +193,7 @@ export default function Page() { /> )} + {needsDepotAddress && ( void optimize()} + isOptimizing={isOptimizing} />
Date: Sat, 23 May 2026 12:47:04 -0700 Subject: [PATCH 050/119] style(edit): modify the design of the available/in-use toggle buttons --- .../vehicle/VehicleDetailsOverlay.tsx | 49 +++++----- .../edit/components/vehicle/VehicleRow.tsx | 89 +++++++++++++------ app/ui/src/app/edit/formStyles.v2.ts | 15 +++- 3 files changed, 103 insertions(+), 50 deletions(-) diff --git a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx index c746a5a4..edd8110f 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx @@ -36,12 +36,12 @@ import { OVERLAY_SELECT_ICON, OVERLAY_SELECT_WRAPPER, OVERLAY_SELECT_WRAPPER_ERROR, - OVERLAY_STATUS_BADGE_AVAILABLE, - OVERLAY_STATUS_BADGE_TEXT_AVAILABLE, - OVERLAY_STATUS_BADGE_TEXT_IN_USE, - OVERLAY_STATUS_BADGE_IN_USE, OVERLAY_STATUS_HINT, OVERLAY_STATUS_ROW, + STATUS_TOGGLE_WRAPPER, + STATUS_TOGGLE_BTN_ACTIVE, + STATUS_TOGGLE_BTN_INACTIVE, + STATUS_TOGGLE_TEXT, OVERLAY_SCROLL_BODY, OVERLAY_TIME_SEGMENTS, OVERLAY_TITLE, @@ -383,27 +383,36 @@ export default function VehicleDetailsOverlay({
Status
- + + In use + +

Tap to toggle

diff --git a/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx b/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx index 77624820..2bed40d4 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx @@ -16,11 +16,11 @@ import { VEHICLE_ROW_CELL, VEHICLE_ROW_ACTIONS, VEHICLE_ROW_DESKTOP, - VEHICLE_ROW_STATUS_BADGE_AVAILABLE, - VEHICLE_ROW_STATUS_BADGE_IN_USE, VEHICLE_ROW_STATUS_CELL, - VEHICLE_ROW_STATUS_TEXT_AVAILABLE, - VEHICLE_ROW_STATUS_TEXT_IN_USE, + STATUS_TOGGLE_WRAPPER, + STATUS_TOGGLE_BTN_ACTIVE, + STATUS_TOGGLE_BTN_INACTIVE, + STATUS_TOGGLE_TEXT, VEHICLE_MOBILE_LOCKED_CARD_V2, VEHICLE_MOBILE_LOCKED_HEADER, VEHICLE_MOBILE_LOCKED_INFO, @@ -59,13 +59,6 @@ export default function VehicleRow({ deleteVehicle, onEditVehicle, }: VehicleRowProps) { - const statusBadge = v.available - ? VEHICLE_ROW_STATUS_BADGE_AVAILABLE - : VEHICLE_ROW_STATUS_BADGE_IN_USE; - const statusText = v.available - ? VEHICLE_ROW_STATUS_TEXT_AVAILABLE - : VEHICLE_ROW_STATUS_TEXT_IN_USE; - if (layout === "mobile") { return (
@@ -87,16 +80,36 @@ export default function VehicleRow({
- + + +
{(v.departureTime || "--:--") + " departure time"} @@ -115,16 +128,34 @@ export default function VehicleRow({ {formatCapacity(v)} - + + +
{v.departureTime}
diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 72280f84..b8927833 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -155,7 +155,7 @@ export const VEHICLE_EMPTY_STATE_TITLE = EMPTY_STATE_TITLE; export const VEHICLE_EMPTY_STATE_SUBTITLE = EMPTY_STATE_SUBTITLE; export const VEHICLE_ROW_DESKTOP = - "grid w-full grid-cols-[minmax(7rem,1.2fr)_minmax(5rem,0.8fr)_minmax(6rem,0.9fr)_minmax(7rem,0.9fr)_minmax(7rem,1fr)_5.25rem] gap-4 items-center"; + "grid w-full grid-cols-[minmax(7rem,1.2fr)_minmax(5rem,0.8fr)_minmax(6rem,0.9fr)_minmax(10.5rem,0.9fr)_minmax(7rem,1fr)_5.25rem] gap-4 items-center"; export const VEHICLE_ROW_CELL = "min-w-0 font-normal text-[16px] leading-[1.5] text-[var(--edit-text-primary)] truncate"; @@ -175,6 +175,19 @@ export const VEHICLE_ROW_STATUS_TEXT_AVAILABLE = export const VEHICLE_ROW_STATUS_TEXT_IN_USE = "font-semibold text-[16px] leading-[22px] text-[var(--edit-text-secondary)] whitespace-nowrap"; +// Segmented toggle for Available / In use (Figma 8724:4604) +export const STATUS_TOGGLE_WRAPPER = + "bg-[var(--edit-stone-50)] flex gap-[4px] items-center p-[2px] rounded-[6px] w-fit"; + +export const STATUS_TOGGLE_BTN_ACTIVE = + "bg-[var(--edit-container-active)] flex items-center px-[8px] py-[6px] rounded-[4px] cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--edit-teal-300)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--edit-bg-primary)]"; + +export const STATUS_TOGGLE_BTN_INACTIVE = + "bg-transparent flex items-center px-[8px] py-[6px] rounded-[4px] cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--edit-teal-300)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--edit-bg-primary)]"; + +export const STATUS_TOGGLE_TEXT = + "font-semibold text-[16px] leading-[22px] text-[var(--edit-text-primary)] whitespace-nowrap"; + export const VEHICLE_ROW_ACTIONS = "flex items-center justify-end gap-1"; export const VEHICLE_ROW_ICON_BUTTON = From bd5299c436bd1c9900a16535534eda1bec39c14d Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 13:12:21 -0700 Subject: [PATCH 051/119] feat(edit): created UI component that shows overlay when user is dragging in files to import --- .../components/shared/DragDropOverlay.tsx | 28 +++++++++++++++++++ app/ui/src/app/edit/edit.module.css | 3 ++ app/ui/src/app/edit/formStyles.v2.ts | 15 +++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 app/ui/src/app/edit/components/shared/DragDropOverlay.tsx diff --git a/app/ui/src/app/edit/components/shared/DragDropOverlay.tsx b/app/ui/src/app/edit/components/shared/DragDropOverlay.tsx new file mode 100644 index 00000000..d626f55b --- /dev/null +++ b/app/ui/src/app/edit/components/shared/DragDropOverlay.tsx @@ -0,0 +1,28 @@ +import { + DRAG_DROP_OVERLAY_CONTENT, + DRAG_DROP_OVERLAY_ICON, + DRAG_DROP_OVERLAY_LABEL, + DRAG_DROP_OVERLAY_ROOT, +} from "@/app/edit/formStyles.v2"; + +export default function DragDropOverlay() { + return ( + + ); +} diff --git a/app/ui/src/app/edit/edit.module.css b/app/ui/src/app/edit/edit.module.css index 01f07bed..c49c9f2a 100644 --- a/app/ui/src/app/edit/edit.module.css +++ b/app/ui/src/app/edit/edit.module.css @@ -77,6 +77,9 @@ --edit-vehicle-light: #fcd34d; --edit-vehicle-back-panel: #312e81; + /* ── Drag-drop overlay ──────────────────────────────────────── */ + --edit-drag-overlay-bg: rgba(213, 242, 232, 0.48); + /* Address illustration */ --edit-address-accent: #f97316; --edit-address-on-accent: #ffffff; diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index b8927833..c15defb1 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -120,7 +120,7 @@ export const PAGE_V2_ROOT = export const PAGE_V2_BODY = "flex flex-1 lg:min-h-0 lg:overflow-hidden"; export const PAGE_V2_MAIN = - "flex-1 min-w-0 bg-[var(--edit-bg-primary)] border-t-0 lg:border-t border-l-0 lg:border-l border-[var(--edit-stone-200)] rounded-none lg:rounded-tl-[12px] p-6 pb-[136px] lg:pb-6 lg:overflow-y-auto space-y-16"; + "relative flex-1 min-w-0 bg-[var(--edit-bg-primary)] border-t-0 lg:border-t border-l-0 lg:border-l border-[var(--edit-stone-200)] rounded-none lg:rounded-tl-[12px] p-6 pb-[136px] lg:pb-6 lg:overflow-y-auto space-y-16"; export const VEHICLE_INFO_CONTAINER = "hidden lg:flex flex-col gap-4 border border-[var(--edit-stone-200)] rounded-[8px] overflow-hidden p-4"; @@ -650,6 +650,19 @@ export const MOBILE_FOOTER_TEXT_LINE = "relative shrink-0"; export const OPTIMIZING_SPINNER_WRAP = "flex justify-center mt-2"; +// ── Drag-Drop Overlay (Figma 8080:3134) ────────────────────────────────────── + +export const DRAG_DROP_OVERLAY_ROOT = + "absolute inset-0 z-50 flex items-center justify-center bg-[var(--edit-drag-overlay-bg)] border-4 border-[var(--edit-teal-600)] rounded-tl-[12px]"; + +export const DRAG_DROP_OVERLAY_CONTENT = + "flex flex-col gap-4 items-center w-[250px]"; + +export const DRAG_DROP_OVERLAY_ICON = "size-20 shrink-0"; + +export const DRAG_DROP_OVERLAY_LABEL = + "font-bold text-[20px] leading-[28px] text-[var(--edit-text-primary)] text-center"; + // ── Mobile Address Card Edit State (Figma 8325:4843) ────────────────────────── export const MOBILE_ADDR_CARD_EDIT_CONTENT = "flex flex-col gap-6"; From 63de6659a2feb2f9965097934dfb6ee99bae1776 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 13:43:12 -0700 Subject: [PATCH 052/119] style(edit): add manage header, change optimize button location, and make vehicle and delivery subheaders smaller --- .../components/layout/ManageSectionHeader.tsx | 30 ++++++++++++++++ .../components/vehicle/VehicleSection.tsx | 20 +---------- app/ui/src/app/edit/edit.module.css | 1 + app/ui/src/app/edit/formStyles.v2.ts | 36 +++++++++++-------- app/ui/src/app/edit/page.tsx | 20 +++++++---- 5 files changed, 67 insertions(+), 40 deletions(-) create mode 100644 app/ui/src/app/edit/components/layout/ManageSectionHeader.tsx diff --git a/app/ui/src/app/edit/components/layout/ManageSectionHeader.tsx b/app/ui/src/app/edit/components/layout/ManageSectionHeader.tsx new file mode 100644 index 00000000..cdea3a5e --- /dev/null +++ b/app/ui/src/app/edit/components/layout/ManageSectionHeader.tsx @@ -0,0 +1,30 @@ +"use client"; + +import styles from "@/app/edit/edit.module.css"; +import { + MANAGE_SECTION_HEADER_ROOT, + MANAGE_SECTION_HEADING, + VEHICLE_SECTION_OPTIMIZE_BTN, +} from "@/app/edit/formStyles.v2"; + +type Props = { + onOptimize: () => void; + isOptimizing: boolean; +}; + +export default function ManageSectionHeader({ onOptimize, isOptimizing }: Props) { + return ( +
+

Manage

+ +
+ ); +} diff --git a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx index 2df10d74..98f2f032 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx @@ -5,7 +5,6 @@ */ import { useState } from "react"; -import styles from "@/app/edit/edit.module.css"; import VehicleRow from "@/app/edit/components/vehicle/VehicleRow"; import VehicleEmptyState from "@/app/edit/components/vehicle/VehicleEmptyState"; import VehicleDetailsOverlay from "@/app/edit/components/vehicle/VehicleDetailsOverlay"; @@ -23,8 +22,6 @@ import { VEHICLE_SECTION_ACTIONS, VEHICLE_SECTION_HEADER, VEHICLE_SECTION_HEADING, - VEHICLE_SECTION_HEADING_ROW, - VEHICLE_SECTION_OPTIMIZE_BTN, VEHICLE_SECTION_SUBHEADING, MOBILE_EMPTY_STATE_CONTAINER, VEHICLE_MOBILE_LIST, @@ -69,8 +66,6 @@ type VehicleSectionProps = { activeVehicleIsValid: boolean; geocodeFailedVehicleIds: number[]; outOfRegionVehicleIds: number[]; - onOptimize: () => void; - isOptimizing: boolean; }; export default function VehicleSection({ @@ -84,8 +79,6 @@ export default function VehicleSection({ activeVehicleIsValid, geocodeFailedVehicleIds, outOfRegionVehicleIds, - onOptimize, - isOptimizing, }: VehicleSectionProps) { const [isAddOverlayOpen, setIsAddOverlayOpen] = useState(false); const [editingVehicle, setEditingVehicle] = useState( @@ -117,18 +110,7 @@ export default function VehicleSection({ return (
-
-

Vehicle details

- -
+

Vehicle details

Manage your delivery fleet

diff --git a/app/ui/src/app/edit/edit.module.css b/app/ui/src/app/edit/edit.module.css index c49c9f2a..42704e0c 100644 --- a/app/ui/src/app/edit/edit.module.css +++ b/app/ui/src/app/edit/edit.module.css @@ -35,6 +35,7 @@ --edit-container-active: #d5f2e8; --edit-text-primary: #272725; --edit-text-secondary: #464544; + --edit-manage-heading: #000000; /* ── Buttons ──────────────────────────────────────────────────── */ --edit-btn-primary: #4cb599; diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index c15defb1..c44a2442 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -109,7 +109,7 @@ export const NAVBAR_V2_BTN_SAVE = "h-9 px-[16px] rounded-[5px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; export const NAVBAR_V2_BTN_OUTLINE = - "h-9 px-4 rounded-[80px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"; + "h-9 px-4 rounded-[5px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"; export const NAVBAR_V2_BTN_FILLED = "h-9 px-4 rounded-[80px] bg-[var(--edit-btn-primary)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap cursor-pointer disabled:cursor-not-allowed"; @@ -232,38 +232,46 @@ export const VEHICLE_SECTION_OPTIMIZE_BTN = export const VEHICLE_SECTION_HEADER = "flex flex-col gap-2 mb-4"; export const VEHICLE_SECTION_HEADING = - "font-bold text-[20px] leading-[28px] text-[var(--edit-text-primary)]"; + "font-[650] text-[16px] leading-[1.5] text-[var(--edit-text-primary)]"; export const VEHICLE_SECTION_SUBHEADING = - "text-[16px] leading-normal text-[var(--edit-text-secondary)]"; + "font-normal text-[16px] leading-[1.5] text-[var(--edit-text-secondary)] whitespace-nowrap"; + +export const MANAGE_SECTION_HEADER_ROOT = + "flex items-center justify-between w-full"; + +export const MANAGE_SECTION_HEADING = + "font-bold text-[20px] leading-[28px] text-[var(--edit-manage-heading)] whitespace-nowrap"; + +export const MANAGE_VEHICLE_GROUP = "flex flex-col gap-4"; export const ADDRESS_SECTION_WITH_PAGINATION = "flex flex-col gap-4"; export const ADDRESS_SECTION_HEADER = "flex flex-col gap-2 mb-4"; export const ADDRESS_SECTION_HEADING = - "font-bold text-[20px] leading-[28px] text-[var(--edit-text-primary)]"; + "font-[650] text-[16px] leading-[1.5] text-[var(--edit-text-primary)]"; export const ADDRESS_SECTION_SUBHEADING = - "text-[16px] leading-normal text-[var(--edit-text-secondary)]"; + "font-normal text-[16px] leading-[1.5] text-[var(--edit-text-secondary)] whitespace-nowrap"; export const ADDRESS_BTN_V2_DESKTOP_ENABLED = - "h-10 px-4 shrink-0 rounded-[80px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; + "h-10 px-4 shrink-0 rounded-[5px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; export const ADDRESS_BTN_V2_DESKTOP_DISABLED = - "h-10 px-4 shrink-0 rounded-[80px] border border-[var(--edit-stone-200)] font-semibold text-[14px] leading-5 text-[var(--edit-stone-500)] whitespace-nowrap cursor-not-allowed opacity-50"; + "h-10 px-4 shrink-0 rounded-[5px] border border-[var(--edit-stone-200)] font-semibold text-[14px] leading-5 text-[var(--edit-stone-500)] whitespace-nowrap overflow-clip cursor-not-allowed opacity-50"; export const ADDRESS_BTN_V2_MOBILE_ENABLED = - "w-full h-10 px-4 rounded-[80px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; + "w-full h-10 px-4 rounded-[5px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; export const ADDRESS_BTN_V2_MOBILE_DISABLED = - "w-full h-10 px-4 rounded-[80px] border border-[var(--edit-stone-200)] font-semibold text-[14px] leading-5 text-[var(--edit-stone-500)] whitespace-nowrap cursor-not-allowed opacity-50"; + "w-full h-10 px-4 rounded-[5px] border border-[var(--edit-stone-200)] font-semibold text-[14px] leading-5 text-[var(--edit-stone-500)] whitespace-nowrap overflow-clip cursor-not-allowed opacity-50"; export const ADDRESS_SEARCH_BAR = - "flex items-center gap-2 px-4 py-[11px] rounded-[80px] border border-[var(--edit-stone-200)] bg-[var(--edit-stone-50)] focus-within:border-[var(--edit-teal-300)] transition-colors"; + "flex items-center gap-2 px-4 py-[11px] rounded-[4px] border border-[var(--edit-stone-200)] bg-[var(--edit-stone-50)] focus-within:border-[var(--edit-teal-300)] transition-colors"; export const ADDRESS_SEARCH_BAR_DESKTOP = - "flex items-center gap-2 h-9 px-4 rounded-[80px] border border-[var(--edit-stone-200)] bg-transparent focus-within:border-[var(--edit-teal-300)] transition-colors"; + "flex items-center gap-2 h-9 px-4 rounded-[4px] border border-[var(--edit-stone-200)] bg-transparent focus-within:border-[var(--edit-teal-300)] transition-colors"; export const ADDRESS_SEARCH_INPUT = "flex-1 font-normal text-[16px] leading-[1.5] text-[var(--edit-text-primary)] placeholder:text-[var(--edit-stone-500)] outline-none bg-transparent min-w-0 [&::-webkit-search-cancel-button]:hidden"; @@ -284,13 +292,13 @@ export const MOBILE_ADDR_TOOLBAR_ROOT = export const MOBILE_ADDR_TOOLBAR_BTN_ROW = "flex gap-2 items-center"; export const MOBILE_ADDR_TOOLBAR_BTN_ENABLED = - "h-9 px-4 shrink-0 rounded-[80px] border border-[var(--edit-text-primary)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; + "h-9 px-4 shrink-0 rounded-[5px] border border-[var(--edit-text-primary)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors cursor-pointer"; export const MOBILE_ADDR_TOOLBAR_BTN_DISABLED = - "h-9 px-4 shrink-0 rounded-[80px] border border-[var(--edit-stone-200)] font-semibold text-[14px] leading-5 text-[var(--edit-stone-500)] whitespace-nowrap cursor-not-allowed opacity-50"; + "h-9 px-4 shrink-0 rounded-[5px] border border-[var(--edit-stone-200)] font-semibold text-[14px] leading-5 text-[var(--edit-stone-500)] whitespace-nowrap overflow-clip cursor-not-allowed opacity-50"; export const ADDRESS_SEARCH_BAR_COMPACT = - "h-9 w-full flex items-center gap-2 px-4 rounded-[80px] border border-[var(--edit-stone-200)] focus-within:border-[var(--edit-teal-300)] transition-colors"; + "h-9 w-full flex items-center gap-2 px-4 rounded-[4px] border border-[var(--edit-stone-200)] focus-within:border-[var(--edit-teal-300)] transition-colors"; // ── Address Row Header (Figma 8012:2303) ────────────────────────────────────── diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index 2d587393..d1ceaac0 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -19,8 +19,10 @@ import { PAGE_V2_BODY, PAGE_V2_MAIN, ADDRESS_SECTION_WITH_PAGINATION, + MANAGE_VEHICLE_GROUP, } from "@/app/edit/formStyles.v2"; import VehicleSection from "@/app/edit/components/vehicle/VehicleSection"; +import ManageSectionHeader from "@/app/edit/components/layout/ManageSectionHeader"; import AddressSection from "@/app/edit/components/address/AddressSection"; import AddressPagination from "@/app/edit/components/address/AddressPagination"; import AddressPaginationMobile from "@/app/edit/components/address/AddressPaginationMobile"; @@ -215,13 +217,17 @@ export default function Page() {
- void optimize()} - isOptimizing={isOptimizing} - /> +
+ void optimize()} + isOptimizing={isOptimizing} + /> + +
Date: Sat, 23 May 2026 18:25:16 -0700 Subject: [PATCH 053/119] refactor(edit): move manageSectionHeader to more appropriate folder --- .../edit/components/{layout => shared}/ManageSectionHeader.tsx | 0 app/ui/src/app/edit/page.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename app/ui/src/app/edit/components/{layout => shared}/ManageSectionHeader.tsx (100%) diff --git a/app/ui/src/app/edit/components/layout/ManageSectionHeader.tsx b/app/ui/src/app/edit/components/shared/ManageSectionHeader.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/ManageSectionHeader.tsx rename to app/ui/src/app/edit/components/shared/ManageSectionHeader.tsx diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index d1ceaac0..b7d948ed 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -22,7 +22,7 @@ import { MANAGE_VEHICLE_GROUP, } from "@/app/edit/formStyles.v2"; import VehicleSection from "@/app/edit/components/vehicle/VehicleSection"; -import ManageSectionHeader from "@/app/edit/components/layout/ManageSectionHeader"; +import ManageSectionHeader from "@/app/edit/components/shared/ManageSectionHeader"; import AddressSection from "@/app/edit/components/address/AddressSection"; import AddressPagination from "@/app/edit/components/address/AddressPagination"; import AddressPaginationMobile from "@/app/edit/components/address/AddressPaginationMobile"; From 9989e374d46e1987d989ad439a76818710c5a468 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 18:36:28 -0700 Subject: [PATCH 054/119] feat(edit): wire up new useCSVImport to address import and remove old useCSVUpload --- .../components/address/AddressSection.tsx | 9 +- app/ui/src/app/edit/hooks/useCSVUpload.ts | 161 ------------------ app/ui/src/app/edit/page.tsx | 18 +- 3 files changed, 14 insertions(+), 174 deletions(-) delete mode 100644 app/ui/src/app/edit/hooks/useCSVUpload.ts diff --git a/app/ui/src/app/edit/components/address/AddressSection.tsx b/app/ui/src/app/edit/components/address/AddressSection.tsx index 6089a7d6..1ef43e8c 100644 --- a/app/ui/src/app/edit/components/address/AddressSection.tsx +++ b/app/ui/src/app/edit/components/address/AddressSection.tsx @@ -54,7 +54,7 @@ type AddressSectionProps = { searchQuery: string; setSearchQuery: (q: string) => void; outOfRegionIds: number[]; - onCSVUpload: (event: React.ChangeEvent) => void; + onOpenImportModal: (file: File) => void; }; export default function AddressSection({ @@ -72,7 +72,7 @@ export default function AddressSection({ searchQuery, setSearchQuery, outOfRegionIds, - onCSVUpload, + onOpenImportModal, }: AddressSectionProps) { const fileInputRef = useRef(null); const [addressToDeleteId, setAddressToDeleteId] = useState( @@ -97,9 +97,10 @@ export default function AddressSection({ { - onCSVUpload(e); + const file = e.target.files?.[0]; + if (file) onOpenImportModal(file); e.target.value = ""; }} className="hidden" diff --git a/app/ui/src/app/edit/hooks/useCSVUpload.ts b/app/ui/src/app/edit/hooks/useCSVUpload.ts deleted file mode 100644 index 80b26c88..00000000 --- a/app/ui/src/app/edit/hooks/useCSVUpload.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * CSV upload hook: parses a CSV file into AddressCard[], - * then bulk-imports them into the edit page state. - */ - -import { useCallback, useState } from "react"; -import Papa from "papaparse"; -import type { AddressCard } from "@/app/edit/types/delivery"; -import { - resolveColumns, - normalizeTimeOption, - bufferSecondsToMinutes, -} from "@/app/edit/utils/csvParserUtils"; -import { hasAtLeastOneLetter } from "@/app/components/AddressGeocoder/utils"; -import { migrateSessionSaveFile } from "@/lib/validation/session.schema"; -import { mapOptimizeRequestToEditState } from "@/app/edit/utils/sessionMapper"; - -type UseCSVUploadArgs = { - importAddresses: (addresses: AddressCard[]) => void; -}; - -export function useCSVUpload({ importAddresses }: UseCSVUploadArgs) { - const [csvError, setCsvError] = useState(null); - - const handleCSVUpload = useCallback( - async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file) return; - - setCsvError(null); - - try { - const addresses = await parseAddressUpload( - file.name, - await file.text(), - ); - importAddresses(addresses); - } catch (err) { - setCsvError( - err instanceof Error - ? err.message - : "An unexpected error occurred while processing the upload.", - ); - } finally { - event.target.value = ""; - } - }, - [importAddresses], - ); - - const clearCsvError = useCallback(() => setCsvError(null), []); - - return { handleCSVUpload, csvError, clearCsvError }; -} - -export function parseAddressUpload( - fileName: string, - content: string, -): Promise { - const normalizedName = fileName.toLowerCase(); - - if (normalizedName.endsWith(".csv")) { - return parseCsvAddressUpload(content); - } - - if (normalizedName.endsWith(".json")) { - return parseJsonAddressUpload(content); - } - - throw new Error( - "Please upload a .csv file or an exported session .json file.", - ); -} - -function parseCsvAddressUpload(content: string): Promise { - return new Promise((resolve, reject) => { - Papa.parse>(content, { - header: true, - skipEmptyLines: true, - complete: (results) => { - try { - const addresses = mapCsvRowsToAddresses( - results.data, - results.meta.fields ?? [], - ); - resolve(addresses); - } catch (error) { - reject(error); - } - }, - error: (error: Error) => { - reject(new Error(`CSV parsing error: ${error.message}`)); - }, - }); - }); -} - -function parseJsonAddressUpload(content: string): Promise { - let parsed: unknown; - - try { - parsed = JSON.parse(content); - } catch { - throw new Error("This file is not valid JSON."); - } - - try { - const session = migrateSessionSaveFile(parsed).data; - return Promise.resolve(mapOptimizeRequestToEditState(session).addresses); - } catch (error) { - throw error instanceof Error - ? error - : new Error("JSON uploads must use the exported session save format."); - } -} - -function mapCsvRowsToAddresses( - rows: Record[], - fields: string[], -): AddressCard[] { - const cols = resolveColumns(fields); - if (!cols.address) { - throw new Error( - 'CSV must contain an "address" column (or similar: "delivery address", "street", "location", "destination").', - ); - } - - const get = (row: Record, key: string) => - (cols[key] ? row[cols[key]!]?.trim() : undefined) ?? ""; - - const addresses: AddressCard[] = []; - let addrId = 1; - - for (const row of rows) { - const address = get(row, "address"); - if (!address || !hasAtLeastOneLetter(address)) continue; - - const timeStart = normalizeTimeOption(get(row, "time_window_start")); - const timeEnd = normalizeTimeOption(get(row, "time_window_end")); - - addresses.push({ - id: addrId++, - locked: true, - editingExisting: false, - recipientName: get(row, "recipient_name"), - phoneNumber: get(row, "phone_number"), - recipientAddress: address, - timeBuffer: bufferSecondsToMinutes(get(row, "time_buffer")), - deliveryTimeStart: timeStart, - deliveryTimeEnd: timeEnd, - deliveryQuantity: parseInt(get(row, "demand_value") || "1", 10) || 1, - notes: get(row, "notes"), - }); - } - - if (addresses.length === 0) { - throw new Error("No valid deliveries found in the uploaded file."); - } - - return addresses; -} diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index b7d948ed..1886599c 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -33,7 +33,6 @@ import { CSVImportModal } from "@/app/edit/components/CSVImportModal"; import { useVehicles } from "@/app/edit/hooks/useVehicles"; import { useAddresses } from "@/app/edit/hooks/useAddresses"; import { useOptimize } from "@/app/edit/hooks/useOptimize"; -import { useCSVUpload } from "@/app/edit/hooks/useCSVUpload"; import { useCSVImport } from "@/app/edit/hooks/useCSVImport"; import { useCallback, useEffect, useState } from "react"; import type { AddressCard } from "@/app/edit/types/delivery"; @@ -77,13 +76,13 @@ export default function Page() { addressState.cacheAddressLocation, ); - const { handleCSVUpload, csvError, clearCsvError } = useCSVUpload({ - importAddresses: addressState.importAddresses, - }); - - // In-page modal for CSV/JSON imports triggered from AddressSection - const { csvData, isImportModalOpen, parseError, closeImportModal } = - useCSVImport(); + const { + csvData, + isImportModalOpen, + parseError, + openImportModal, + closeImportModal, + } = useCSVImport(); useEffect(() => { let cancelled = false; @@ -196,6 +195,7 @@ export default function Page() { )} + {needsDepotAddress && ( From 13751f39d7df9a73b9dd95ba4b63db875bb59c10 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 18:45:47 -0700 Subject: [PATCH 055/119] refactor(edit): move CSVImportModal to the address folder --- .../src/app/edit/components/{ => address}/CSVImportModal.tsx | 4 ++-- app/ui/src/app/edit/page.tsx | 4 ++-- app/ui/src/app/upload-save-point/page.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename app/ui/src/app/edit/components/{ => address}/CSVImportModal.tsx (99%) diff --git a/app/ui/src/app/edit/components/CSVImportModal.tsx b/app/ui/src/app/edit/components/address/CSVImportModal.tsx similarity index 99% rename from app/ui/src/app/edit/components/CSVImportModal.tsx rename to app/ui/src/app/edit/components/address/CSVImportModal.tsx index 57a40466..8fd8d0ad 100644 --- a/app/ui/src/app/edit/components/CSVImportModal.tsx +++ b/app/ui/src/app/edit/components/address/CSVImportModal.tsx @@ -1,4 +1,4 @@ -// app/edit/components/CSVImportModal.tsx +// app/edit/components/address/CSVImportModal.tsx "use client"; /** @@ -18,7 +18,7 @@ import { useState, useCallback, useMemo } from "react"; import { useRouter } from "next/navigation"; -import type { AddressCard } from "../types/delivery"; +import type { AddressCard } from "@/app/edit/types/delivery"; // ─── Types ──────────────────────────────────────────────────────────────────── diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index 1886599c..d8778a51 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -29,7 +29,7 @@ import AddressPaginationMobile from "@/app/edit/components/address/AddressPagina import EditPageFooter from "@/app/edit/components/layout/footer/EditPageFooter"; import MobileEditPageFooter from "@/app/edit/components/layout/footer/MobileEditPageFooter"; import MobileBottomBar from "@/app/edit/components/layout/navbar/MobileBottomBar"; -import { CSVImportModal } from "@/app/edit/components/CSVImportModal"; +import { CSVImportModal } from "@/app/edit/components/address/CSVImportModal"; import { useVehicles } from "@/app/edit/hooks/useVehicles"; import { useAddresses } from "@/app/edit/hooks/useAddresses"; import { useOptimize } from "@/app/edit/hooks/useOptimize"; @@ -188,7 +188,7 @@ export default function Page() { + importAddresses={(cards: AddressCard[]) => addressState.importAddresses(reindexAddresses(cards)) } /> diff --git a/app/ui/src/app/upload-save-point/page.tsx b/app/ui/src/app/upload-save-point/page.tsx index 0f76d4d3..d4d7a821 100644 --- a/app/ui/src/app/upload-save-point/page.tsx +++ b/app/ui/src/app/upload-save-point/page.tsx @@ -6,7 +6,7 @@ export const dynamic = "force-dynamic"; import { useState, useRef, useCallback } from "react"; import { useRouter } from "next/navigation"; import ShellNavbar from "@/app/components/ShellNavbar"; -import { CSVImportModal } from "@/app/edit/components/CSVImportModal"; +import { CSVImportModal } from "@/app/edit/components/address/CSVImportModal"; import { useCSVImport } from "@/app/edit/hooks/useCSVImport"; import { migrateSessionSaveFile } from "@/lib/validation/session.schema"; import { formatSize } from "@/app/utils/routeUtils"; From f1d9e585f7fbe704833acee461f24ddacfb273c5 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 19:06:51 -0700 Subject: [PATCH 056/119] fix(edit): remove unused imports and wire up save button --- app/ui/src/app/edit/page.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index d8778a51..7b0a5f1f 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -42,7 +42,6 @@ import { mapEditStateToOptimizeRequest, mapOptimizeRequestToEditState, } from "@/app/edit/utils/sessionMapper"; -import { useRouter } from "next/navigation"; import AddressOverlay, { type LocationAddress, } from "@/app/edit/components/address/AddressOverlay"; @@ -50,7 +49,6 @@ import AddressOverlay, { type StoredUploadFile = { name: string; content: string }; export default function Page() { - const router = useRouter(); const vehicleState = useVehicles(); const addressState = useAddresses(); const [sessionError, setSessionError] = useState(null); @@ -144,12 +142,6 @@ export default function Page() { }; }, [importAddresses, importVehicles]); - // Routes to /upload-save-point so the user can upload a .json save file - // or a .csv/.json address list through the column-mapper modal flow. - const handleImportSession = useCallback(() => { - router.push("/upload-save-point"); - }, [router]); - const handleExportSession = useCallback(async () => { setSessionError(null); try { @@ -196,6 +188,7 @@ export default function Page() { + {needsDepotAddress && ( setIsMobileMenuOpen(false)} /> - {}} /> + setIsMobileMenuOpen(true)} /> - {}} /> +
From 40b55dac71e2085c47009a06c900f1ebf08c07dd Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 19:36:29 -0700 Subject: [PATCH 057/119] fix(edit): address start location not showing in results page by wiring up the start location to be displayed in the results page --- app/ui/src/app/edit/hooks/useOptimize.ts | 1 + app/ui/src/app/edit/utils/vroomToRoutes.ts | 12 +++++ app/ui/src/app/results/components/Map.tsx | 46 ++++++++++++++++++- app/ui/src/app/results/components/Sidebar.tsx | 17 +++++++ app/ui/src/app/results/types.ts | 1 + 5 files changed, 76 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/hooks/useOptimize.ts b/app/ui/src/app/edit/hooks/useOptimize.ts index 8f81a0fa..91ebcba4 100644 --- a/app/ui/src/app/edit/hooks/useOptimize.ts +++ b/app/ui/src/app/edit/hooks/useOptimize.ts @@ -362,6 +362,7 @@ export function useOptimize( resultBody as VroomResponse, lockedVehicles, addresses, + trimmedDepotAddress!, ); setOptimizeResults(routes); router.push("/results"); diff --git a/app/ui/src/app/edit/utils/vroomToRoutes.ts b/app/ui/src/app/edit/utils/vroomToRoutes.ts index 8eee8170..1f58bd04 100644 --- a/app/ui/src/app/edit/utils/vroomToRoutes.ts +++ b/app/ui/src/app/edit/utils/vroomToRoutes.ts @@ -38,6 +38,7 @@ export function vroomToRoutes( vroomResponse: VroomResponse, vehicles: VehicleRow[], addresses: AddressCard[], + depotAddress: string, ): Route[] { const vehicleById = new Map(vehicles.map((v) => [String(v.id), v])); const addressById = new Map(addresses.map((a) => [String(a.id), a])); @@ -45,6 +46,10 @@ export function vroomToRoutes( return vroomResponse.routes.map((vroomRoute: VroomRoute): Route => { const vehicle = vehicleById.get(vroomRoute.vehicle_external_id); + const startStep = vroomRoute.steps.find( + (s: VroomStep) => s.type === "start", + ); + const jobSteps = vroomRoute.steps.filter( (s: VroomStep) => s.type === "job" && s.job_external_id != null, ); @@ -85,6 +90,13 @@ export function vroomToRoutes( vehicleType: vehicle?.type || undefined, distanceMi: Math.round(vroomRoute.distance * METERS_TO_MILES * 10) / 10, estimatedTimeMinutes: Math.round(vroomRoute.duration / 60), + startLocation: startStep + ? { + lat: startStep.location[1], + lng: startStep.location[0], + address: vehicle?.startLocation || depotAddress || "", + } + : undefined, }; }); } diff --git a/app/ui/src/app/results/components/Map.tsx b/app/ui/src/app/results/components/Map.tsx index 6d03ad37..a90b96c8 100644 --- a/app/ui/src/app/results/components/Map.tsx +++ b/app/ui/src/app/results/components/Map.tsx @@ -57,7 +57,7 @@ function buildRoutePath( pendingPinMove: PendingPinMove | null, ): google.maps.LatLngLiteral[] { const sorted = [...route.stops].sort((a, b) => a.sequence - b.sequence); - return sorted.map((s) => { + const deliveryPoints = sorted.map((s) => { if ( pendingPinMove?.vehicleId === route.vehicleId && pendingPinMove.stopId === s.id @@ -66,6 +66,13 @@ function buildRoutePath( } return { lat: s.lat, lng: s.lng }; }); + if (route.startLocation) { + return [ + { lat: route.startLocation.lat, lng: route.startLocation.lng }, + ...deliveryPoints, + ]; + } + return deliveryPoints; } function RoutePolylinesOverlay({ @@ -308,6 +315,25 @@ function AdvancedMarkers({ if (cancelled) return; routes.forEach((route) => { + // Depot marker — distinct non-draggable pin labeled "S" + if (route.startLocation) { + const depotEl = document.createElement("div"); + depotEl.style.cssText = + "display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;background:#374151;color:#fff;font-size:11px;font-weight:700;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,0.4)"; + depotEl.textContent = "S"; + const depotMarker = new AdvancedMarkerElement({ + map, + position: { + lat: route.startLocation.lat, + lng: route.startLocation.lng, + }, + title: route.startLocation.address || "Starting point", + content: depotEl, + gmpDraggable: false, + }); + markers.push(depotMarker); + } + const sorted = [...route.stops].sort( (a, b) => a.sequence - b.sequence, ); @@ -403,6 +429,12 @@ export default function MapComponent({ const bounds = new google.maps.LatLngBounds(); routes.forEach((route) => { route.stops.forEach((s) => bounds.extend({ lat: s.lat, lng: s.lng })); + if (route.startLocation) { + bounds.extend({ + lat: route.startLocation.lat, + lng: route.startLocation.lng, + }); + } }); mapInstance.fitBounds(bounds, 48); }, @@ -472,6 +504,18 @@ export default function MapComponent({ ); return ( + {route.startLocation && ( + + )} {sorted.map((stop) => { const atPending = pendingPinMove != null && diff --git a/app/ui/src/app/results/components/Sidebar.tsx b/app/ui/src/app/results/components/Sidebar.tsx index 19932032..5c377465 100644 --- a/app/ui/src/app/results/components/Sidebar.tsx +++ b/app/ui/src/app/results/components/Sidebar.tsx @@ -173,6 +173,23 @@ export default function Sidebar({ {isExpanded && (
    + {route.startLocation && ( +
  • +
    + + S + +
    +

    + Starting point +

    +

    + {route.startLocation.address || "Depot"} +

    +
    +
    +
  • + )} {sortedStops.map((stop) => (
  • Date: Sat, 23 May 2026 21:18:08 -0700 Subject: [PATCH 058/119] refactor(edit): move sidebar and navbar outside of edit page so results page can integrate both components --- .../navbar/MobileBottomBar.tsx | 0 .../navbar/MobileNavbar.tsx | 0 .../layout => components}/navbar/Navbar.tsx | 0 .../sidebar/MobileSidebar.tsx | 0 .../layout => components}/sidebar/Sidebar.tsx | 0 .../sidebar/SidebarEditButton.tsx | 0 .../sidebar/SidebarResultsButton.tsx | 0 .../{layout => }/footer/EditPageFooter.tsx | 0 .../footer/MobileEditPageFooter.tsx | 0 app/ui/src/app/edit/page.tsx | 18 +++++++++--------- 10 files changed, 9 insertions(+), 9 deletions(-) rename app/ui/src/app/{edit/components/layout => components}/navbar/MobileBottomBar.tsx (100%) rename app/ui/src/app/{edit/components/layout => components}/navbar/MobileNavbar.tsx (100%) rename app/ui/src/app/{edit/components/layout => components}/navbar/Navbar.tsx (100%) rename app/ui/src/app/{edit/components/layout => components}/sidebar/MobileSidebar.tsx (100%) rename app/ui/src/app/{edit/components/layout => components}/sidebar/Sidebar.tsx (100%) rename app/ui/src/app/{edit/components/layout => components}/sidebar/SidebarEditButton.tsx (100%) rename app/ui/src/app/{edit/components/layout => components}/sidebar/SidebarResultsButton.tsx (100%) rename app/ui/src/app/edit/components/{layout => }/footer/EditPageFooter.tsx (100%) rename app/ui/src/app/edit/components/{layout => }/footer/MobileEditPageFooter.tsx (100%) diff --git a/app/ui/src/app/edit/components/layout/navbar/MobileBottomBar.tsx b/app/ui/src/app/components/navbar/MobileBottomBar.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/navbar/MobileBottomBar.tsx rename to app/ui/src/app/components/navbar/MobileBottomBar.tsx diff --git a/app/ui/src/app/edit/components/layout/navbar/MobileNavbar.tsx b/app/ui/src/app/components/navbar/MobileNavbar.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/navbar/MobileNavbar.tsx rename to app/ui/src/app/components/navbar/MobileNavbar.tsx diff --git a/app/ui/src/app/edit/components/layout/navbar/Navbar.tsx b/app/ui/src/app/components/navbar/Navbar.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/navbar/Navbar.tsx rename to app/ui/src/app/components/navbar/Navbar.tsx diff --git a/app/ui/src/app/edit/components/layout/sidebar/MobileSidebar.tsx b/app/ui/src/app/components/sidebar/MobileSidebar.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/sidebar/MobileSidebar.tsx rename to app/ui/src/app/components/sidebar/MobileSidebar.tsx diff --git a/app/ui/src/app/edit/components/layout/sidebar/Sidebar.tsx b/app/ui/src/app/components/sidebar/Sidebar.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/sidebar/Sidebar.tsx rename to app/ui/src/app/components/sidebar/Sidebar.tsx diff --git a/app/ui/src/app/edit/components/layout/sidebar/SidebarEditButton.tsx b/app/ui/src/app/components/sidebar/SidebarEditButton.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/sidebar/SidebarEditButton.tsx rename to app/ui/src/app/components/sidebar/SidebarEditButton.tsx diff --git a/app/ui/src/app/edit/components/layout/sidebar/SidebarResultsButton.tsx b/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/sidebar/SidebarResultsButton.tsx rename to app/ui/src/app/components/sidebar/SidebarResultsButton.tsx diff --git a/app/ui/src/app/edit/components/layout/footer/EditPageFooter.tsx b/app/ui/src/app/edit/components/footer/EditPageFooter.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/footer/EditPageFooter.tsx rename to app/ui/src/app/edit/components/footer/EditPageFooter.tsx diff --git a/app/ui/src/app/edit/components/layout/footer/MobileEditPageFooter.tsx b/app/ui/src/app/edit/components/footer/MobileEditPageFooter.tsx similarity index 100% rename from app/ui/src/app/edit/components/layout/footer/MobileEditPageFooter.tsx rename to app/ui/src/app/edit/components/footer/MobileEditPageFooter.tsx diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index 7b0a5f1f..378c58d4 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -6,14 +6,14 @@ */ import styles from "@/app/edit/edit.module.css"; -import Navbar from "@/app/edit/components/layout/navbar/Navbar"; -import MobileNavbar from "@/app/edit/components/layout/navbar/MobileNavbar"; -import MobileSidebar from "@/app/edit/components/layout/sidebar/MobileSidebar"; +import Navbar from "@/app/components/navbar/Navbar"; +import MobileNavbar from "@/app/components/navbar/MobileNavbar"; +import MobileSidebar from "@/app/components/sidebar/MobileSidebar"; import OptimizingModal from "@/app/edit/components/shared/OptimizingModal"; import ErrorPopup from "@/app/edit/components/shared/ErrorPopup"; -import Sidebar from "@/app/edit/components/layout/sidebar/Sidebar"; -import SidebarEditButton from "@/app/edit/components/layout/sidebar/SidebarEditButton"; -import SidebarResultsButton from "@/app/edit/components/layout/sidebar/SidebarResultsButton"; +import Sidebar from "@/app/components/sidebar/Sidebar"; +import SidebarEditButton from "@/app/components/sidebar/SidebarEditButton"; +import SidebarResultsButton from "@/app/components/sidebar/SidebarResultsButton"; import { PAGE_V2_ROOT, PAGE_V2_BODY, @@ -26,9 +26,9 @@ import ManageSectionHeader from "@/app/edit/components/shared/ManageSectionHeade import AddressSection from "@/app/edit/components/address/AddressSection"; import AddressPagination from "@/app/edit/components/address/AddressPagination"; import AddressPaginationMobile from "@/app/edit/components/address/AddressPaginationMobile"; -import EditPageFooter from "@/app/edit/components/layout/footer/EditPageFooter"; -import MobileEditPageFooter from "@/app/edit/components/layout/footer/MobileEditPageFooter"; -import MobileBottomBar from "@/app/edit/components/layout/navbar/MobileBottomBar"; +import EditPageFooter from "@/app/edit/components/footer/EditPageFooter"; +import MobileEditPageFooter from "@/app/edit/components/footer/MobileEditPageFooter"; +import MobileBottomBar from "@/app/components/navbar/MobileBottomBar"; import { CSVImportModal } from "@/app/edit/components/address/CSVImportModal"; import { useVehicles } from "@/app/edit/hooks/useVehicles"; import { useAddresses } from "@/app/edit/hooks/useAddresses"; From d656397d790528962ecc534879aef3f83872d21a Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 22:06:37 -0700 Subject: [PATCH 059/119] feat(edit): add inline errors for the addressCard --- .../edit/components/address/AddressCard.tsx | 71 ++++++++++--------- .../components/address/AddressOverlay.tsx | 12 ++-- .../{OverlayFieldError.tsx => FieldError.tsx} | 4 +- .../vehicle/VehicleDetailsOverlay.tsx | 12 ++-- app/ui/src/app/edit/formStyles.v2.ts | 4 +- 5 files changed, 53 insertions(+), 50 deletions(-) rename app/ui/src/app/edit/components/shared/{OverlayFieldError.tsx => FieldError.tsx} (88%) diff --git a/app/ui/src/app/edit/components/address/AddressCard.tsx b/app/ui/src/app/edit/components/address/AddressCard.tsx index fa43eca7..8c602e5c 100644 --- a/app/ui/src/app/edit/components/address/AddressCard.tsx +++ b/app/ui/src/app/edit/components/address/AddressCard.tsx @@ -15,7 +15,6 @@ import { ADDRESS_ROW_NAME_ROW, ADDRESS_ROW_FIELD_INPUT_FILL, ADDRESS_ROW_ADDR_WRAP, - ADDRESS_ROW_ADDR_WRAP_ERROR, ADDRESS_ROW_ADDR_GRADIENT, ADDRESS_ROW_ADDR_TRIGGER_TEXT, ADDRESS_ROW_ADDR_TRIGGER_PLACEHOLDER, @@ -69,7 +68,9 @@ import { ADDRESS_CARD_MOBILE_ROOT, MOBILE_ADDR_EXPANDED_PANEL, MOBILE_ADDR_LOCKED_NOTES_CLAMP, + ADDRESS_ROW_QTY_FIELD_COL, } from "@/app/edit/formStyles.v2"; +import FieldError from "@/app/edit/components/shared/FieldError"; import { EditIconButton, ConfirmIconButton, @@ -239,8 +240,8 @@ export default function AddressCard({ const startIdx = TIME_OPTIONS.indexOf(a.deliveryTimeStart); const endIdx = TIME_OPTIONS.indexOf(a.deliveryTimeEnd); - const addrInvalid = - geocodeFailed || (addressTouched && !a.recipientAddress.trim()); + const addressMissing = addressTouched && !a.recipientAddress.trim(); + const qtyInvalid = addressTouched && a.deliveryQuantity <= 0; const panelId = `addr-panel-${a.id}`; @@ -355,11 +356,7 @@ export default function AddressCard({
+ {addressMissing && ( + + )}
{/* Quantity — edit */} - updateAddress(a.id, "deliveryQuantity", v)} - onIncrement={() => - updateAddress( - a.id, - "deliveryQuantity", - Math.min(1_000_000, (a.deliveryQuantity || 0) + 1), - ) - } - onDecrement={() => - updateAddress( - a.id, - "deliveryQuantity", - Math.max(1, (a.deliveryQuantity || 1) - 1), - ) - } - /> +
+ updateAddress(a.id, "deliveryQuantity", v)} + onIncrement={() => + updateAddress( + a.id, + "deliveryQuantity", + Math.min(1_000_000, (a.deliveryQuantity || 0) + 1), + ) + } + onDecrement={() => + updateAddress( + a.id, + "deliveryQuantity", + Math.max(1, (a.deliveryQuantity || 1) - 1), + ) + } + /> + {qtyInvalid && } +
{/* Delivery estimation — edit */}
@@ -763,11 +766,7 @@ export default function AddressCard({
+ {addressMissing && ( + + )}
{/* Delivery Info */} @@ -817,6 +819,7 @@ export default function AddressCard({ ) } /> + {qtyInvalid && }
diff --git a/app/ui/src/app/edit/components/address/AddressOverlay.tsx b/app/ui/src/app/edit/components/address/AddressOverlay.tsx index fe74cbe1..d88d9c51 100644 --- a/app/ui/src/app/edit/components/address/AddressOverlay.tsx +++ b/app/ui/src/app/edit/components/address/AddressOverlay.tsx @@ -10,7 +10,7 @@ import { import type { CSSProperties } from "react"; import { useFocusTrap } from "@/app/edit/hooks/useFocusTrap"; import { SUPPORTED_STATES } from "@/app/edit/constants/supportedRegions"; -import OverlayFieldError from "@/app/edit/components/shared/OverlayFieldError"; +import FieldError from "@/app/edit/components/shared/FieldError"; import OverlayAutocompleteDropdown from "@/app/edit/components/shared/OverlayAutocompleteDropdown"; import { useAddressAutocomplete } from "@/app/components/AddressGeocoder/utils/useAddressAutocomplete"; import type { AddressSuggestion } from "@/app/components/AddressGeocoder/types"; @@ -269,7 +269,7 @@ export default function AddressOverlay({ /> )}
- {line1Error && } + {line1Error && }
{/* Address line 2 — full width, optional */} @@ -305,7 +305,7 @@ export default function AddressOverlay({ aria-required="true" aria-invalid={cityError} /> - {cityError && } + {cityError && }
@@ -350,7 +350,7 @@ export default function AddressOverlay({ ))}
- {stateError && } + {stateError && }
@@ -377,7 +377,7 @@ export default function AddressOverlay({ aria-invalid={zipError} /> {zipError && ( - + )}
@@ -426,7 +426,7 @@ export default function AddressOverlay({
{countryError && ( - + )}
diff --git a/app/ui/src/app/edit/components/shared/OverlayFieldError.tsx b/app/ui/src/app/edit/components/shared/FieldError.tsx similarity index 88% rename from app/ui/src/app/edit/components/shared/OverlayFieldError.tsx rename to app/ui/src/app/edit/components/shared/FieldError.tsx index d18851c5..bcc108b9 100644 --- a/app/ui/src/app/edit/components/shared/OverlayFieldError.tsx +++ b/app/ui/src/app/edit/components/shared/FieldError.tsx @@ -29,11 +29,11 @@ const WARNING_ICON = ( ); -type OverlayFieldErrorProps = { +type FieldErrorProps = { message: string; }; -export default function OverlayFieldError({ message }: OverlayFieldErrorProps) { +export default function FieldError({ message }: FieldErrorProps) { return (
{WARNING_ICON} diff --git a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx index edd8110f..b45ce4bb 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx @@ -7,7 +7,7 @@ import type { CapacityUnit, } from "@/app/edit/types/delivery"; import { useFocusTrap } from "@/app/edit/hooks/useFocusTrap"; -import OverlayFieldError from "@/app/edit/components/shared/OverlayFieldError"; +import FieldError from "@/app/edit/components/shared/FieldError"; import { OVERLAY_BACKDROP, OVERLAY_BODY, @@ -247,7 +247,7 @@ export default function VehicleDetailsOverlay({ aria-required="true" aria-invalid={nameError} /> - {nameError && } + {nameError && }
@@ -293,7 +293,7 @@ export default function VehicleDetailsOverlay({
{typeError && ( - + )} @@ -325,7 +325,7 @@ export default function VehicleDetailsOverlay({ aria-invalid={capacityError} /> {capacityError && ( - + )} @@ -374,7 +374,7 @@ export default function VehicleDetailsOverlay({ - {unitError && } + {unitError && } @@ -508,7 +508,7 @@ export default function VehicleDetailsOverlay({ {departureError && ( - + )} diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index c44a2442..69bec21c 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -357,8 +357,6 @@ export const ADDRESS_ROW_FIELD_INPUT_FILL = `${ADDRESS_ROW_FIELD_INPUT} flex-1`; export const ADDRESS_ROW_ADDR_WRAP = "relative border border-[var(--edit-stone-200)] flex h-11 items-center rounded-[6px] overflow-hidden w-full cursor-pointer"; -export const ADDRESS_ROW_ADDR_WRAP_ERROR = `${ADDRESS_ROW_ADDR_WRAP} border-[var(--edit-error-border)]`; - export const ADDRESS_ROW_ADDR_GRADIENT = "pointer-events-none absolute right-0 top-0 h-full w-[72px] bg-gradient-to-l from-[var(--edit-bg-primary)] from-[60%] to-transparent flex items-center justify-end pr-2"; @@ -373,6 +371,8 @@ export const ADDRESS_ROW_STEPPER_CONTAINER = export const ADDRESS_ROW_STEPPER_CONTAINER_NARROW = `${ADDRESS_ROW_STEPPER_CONTAINER} w-[72px]`; +export const ADDRESS_ROW_QTY_FIELD_COL = "flex flex-col gap-2"; + export const ADDRESS_ROW_STEPPER_INPUT = "flex-1 min-w-0 bg-transparent outline-none text-[16px] leading-[1.5] text-[var(--edit-text-primary)] [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"; From a807aad6f5976a82e66436e77bda57e970255b66 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 22:14:25 -0700 Subject: [PATCH 060/119] refactor(edit): rename ErrorPopup to OptimizeErrorPopup for clarity --- .../shared/{ErrorPopup.tsx => OptimizeErrorPopup.tsx} | 4 ++-- app/ui/src/app/edit/page.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename app/ui/src/app/edit/components/shared/{ErrorPopup.tsx => OptimizeErrorPopup.tsx} (93%) diff --git a/app/ui/src/app/edit/components/shared/ErrorPopup.tsx b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx similarity index 93% rename from app/ui/src/app/edit/components/shared/ErrorPopup.tsx rename to app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx index c2b88694..663dd585 100644 --- a/app/ui/src/app/edit/components/shared/ErrorPopup.tsx +++ b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx @@ -15,12 +15,12 @@ import { } from "@/app/edit/formStyles"; import { useFocusTrap } from "@/app/edit/hooks/useFocusTrap"; -type ErrorPopupProps = { +type OptimizeErrorPopupProps = { message: string | null; onClose: () => void; }; -export default function ErrorPopup({ message, onClose }: ErrorPopupProps) { +export default function OptimizeErrorPopup({ message, onClose }: OptimizeErrorPopupProps) { const panelRef = useFocusTrap(!!message); const handleKeyDown = (e: React.KeyboardEvent) => { diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index 378c58d4..a356c6ae 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -10,7 +10,7 @@ import Navbar from "@/app/components/navbar/Navbar"; import MobileNavbar from "@/app/components/navbar/MobileNavbar"; import MobileSidebar from "@/app/components/sidebar/MobileSidebar"; import OptimizingModal from "@/app/edit/components/shared/OptimizingModal"; -import ErrorPopup from "@/app/edit/components/shared/ErrorPopup"; +import OptimizeErrorPopup from "@/app/edit/components/shared/OptimizeErrorPopup"; import Sidebar from "@/app/components/sidebar/Sidebar"; import SidebarEditButton from "@/app/components/sidebar/SidebarEditButton"; import SidebarResultsButton from "@/app/components/sidebar/SidebarResultsButton"; @@ -186,9 +186,9 @@ export default function Page() { /> )} - - - + + + {needsDepotAddress && ( Date: Sat, 23 May 2026 22:24:46 -0700 Subject: [PATCH 061/119] style(edit): upgrade optimizeErrorPopup from mid-fi to hi-fi design --- .../components/shared/OptimizeErrorPopup.tsx | 87 ++++++++++--------- app/ui/src/app/edit/formStyles.v2.ts | 7 ++ 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx index 663dd585..ba1baab6 100644 --- a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx +++ b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx @@ -1,18 +1,16 @@ "use client"; -/** - * Modal popup for surfacing a single error message to the user. - * Renders nothing when `message` is null. - */ - import { - ERROR_POPUP_CLOSE_ICON, - ERROR_POPUP_DISMISS_BUTTON, - MODAL_MESSAGE, - MODAL_OVERLAY, - MODAL_PANEL, - MODAL_TITLE, -} from "@/app/edit/formStyles"; + ERROR_POPUP_FOOTER, + ERROR_POPUP_MESSAGE, + OVERLAY_BACKDROP, + OVERLAY_CLOSE_BTN, + OVERLAY_HEADER, + OVERLAY_PANEL, + OVERLAY_PRIMARY_BTN, + OVERLAY_TITLE, +} from "@/app/edit/formStyles.v2"; +import styles from "@/app/edit/edit.module.css"; import { useFocusTrap } from "@/app/edit/hooks/useFocusTrap"; type OptimizeErrorPopupProps = { @@ -33,45 +31,48 @@ export default function OptimizeErrorPopup({ message, onClose }: OptimizeErrorPo return (
-
- -

- Something went wrong -

-

{message}

- + aria-hidden + > + + + + +
+

{message}

+
+ +
); diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 69bec21c..e220400b 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -759,3 +759,10 @@ export const MOBILE_BOTTOM_BAR_SECONDARY_BTN = export const MOBILE_BOTTOM_BAR_SECONDARY_LABEL = "font-['Manrope',sans-serif] font-semibold text-[16px] leading-[22px] text-[var(--edit-text-primary)] whitespace-nowrap"; + +// ── OptimizeErrorPopup ──────────────────────────────────────────────────────── + +export const ERROR_POPUP_MESSAGE = + "text-[14px] leading-5 text-[var(--edit-text-secondary)] w-full"; + +export const ERROR_POPUP_FOOTER = "flex items-center justify-end"; From ed7b533b1d20652c52e4b9d251563a96c0425db9 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 23:55:43 -0700 Subject: [PATCH 062/119] feat(edit): create new ui component for csv upload overlay --- .../components/address/AddressSection.tsx | 32 ++- .../components/address/CSVUploadOverlay.tsx | 222 ++++++++++++++++++ app/ui/src/app/edit/formStyles.v2.ts | 42 ++++ 3 files changed, 279 insertions(+), 17 deletions(-) create mode 100644 app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx diff --git a/app/ui/src/app/edit/components/address/AddressSection.tsx b/app/ui/src/app/edit/components/address/AddressSection.tsx index 1ef43e8c..90f051c8 100644 --- a/app/ui/src/app/edit/components/address/AddressSection.tsx +++ b/app/ui/src/app/edit/components/address/AddressSection.tsx @@ -4,11 +4,12 @@ * Addresses region: toolbar (find / add / import) and a stacked list of delivery cards for the current page. */ -import { useRef, useState } from "react"; +import { useState } from "react"; import AddressCard from "@/app/edit/components/address/AddressCard"; import ConfirmDeletionOverlay from "@/app/edit/components/shared/ConfirmDeletionOverlay"; import AddressEmptyState from "@/app/edit/components/address/AddressEmptyState"; import AddressRowHeader from "@/app/edit/components/address/AddressRowHeader"; +import CSVUploadOverlay from "@/app/edit/components/address/CSVUploadOverlay"; import type { AddressCard as AddressCardType } from "@/app/edit/types/delivery"; import { ADDRESS_EMPTY_STATE, @@ -74,7 +75,7 @@ export default function AddressSection({ outOfRegionIds, onOpenImportModal, }: AddressSectionProps) { - const fileInputRef = useRef(null); + const [isUploadOverlayOpen, setIsUploadOverlayOpen] = useState(false); const [addressToDeleteId, setAddressToDeleteId] = useState( null, ); @@ -94,19 +95,6 @@ export default function AddressSection({

- { - const file = e.target.files?.[0]; - if (file) onOpenImportModal(file); - e.target.value = ""; - }} - className="hidden" - aria-hidden="true" - /> - {/* Mobile: Search top, buttons right-aligned side-by-side (Figma 8325:7503) */}
+ {isUploadOverlayOpen && ( + setIsUploadOverlayOpen(false)} + onFileSelect={(file) => { + onOpenImportModal(file); + setIsUploadOverlayOpen(false); + }} + /> + )} + {addressToDeleteId !== null && ( void; + onFileSelect: (file: File) => void; +}; + +function formatFileSize(bytes: number): string { + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +export default function CSVUploadOverlay({ + onClose, + onFileSelect, +}: CSVUploadOverlayProps) { + const fileInputRef = useRef(null); + const [selectedFile, setSelectedFile] = useState(null); + + function handleFileChange(e: React.ChangeEvent) { + const file = e.target.files?.[0] ?? null; + setSelectedFile(file); + e.target.value = ""; + } + + function handleRemoveFile() { + setSelectedFile(null); + } + + function handleNext() { + if (selectedFile) onFileSelect(selectedFile); + } + + return ( +
+
e.stopPropagation()} + role="dialog" + aria-modal="true" + aria-labelledby="csv-upload-title" + > +
+
+
+ {/* Header */} +
+

+ Import from CSV +

+ +
+ + {/* Drop zone */} +
+
+ + +
+

+ Drag and drop CSV files here, or +

+ +
+
+ + +
+ + {/* Description */} +

+ Import delivery details from a CSV file. Maximum file size of X + MB. +

+
+ + {/* File chip — visible only when a file is selected */} + {selectedFile !== null && ( +
+
+ +

+ {selectedFile.name} +

+
+ +
+

+ {formatFileSize(selectedFile.size)} +

+ +
+
+ )} +
+ + {/* Footer */} +
+ + +
+
+
+
+ ); +} diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index e220400b..7d809f87 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -766,3 +766,45 @@ export const ERROR_POPUP_MESSAGE = "text-[14px] leading-5 text-[var(--edit-text-secondary)] w-full"; export const ERROR_POPUP_FOOTER = "flex items-center justify-end"; + +// ── CSV Upload Overlay (Figma 8102:1548 desktop / 7472:5765 mobile) ─────────── + +export const CSV_UPLOAD_OVERLAY_INNER = + "flex flex-col gap-[16px] items-end w-full"; + +export const CSV_UPLOAD_OVERLAY_CONTENT = + "flex flex-col gap-[16px] items-center w-full"; + +export const CSV_UPLOAD_OVERLAY_TOP = + "flex flex-col gap-[24px] items-start w-full"; + +export const CSV_UPLOAD_DROP_ZONE = + "bg-[var(--edit-stone-50)] border border-[var(--edit-stone-200)] border-dashed flex flex-col h-[200px] items-center justify-center overflow-clip pt-[24px] pb-[16px] rounded-[6px] w-full"; + +export const CSV_UPLOAD_DROP_ZONE_INNER = + "flex flex-col gap-[8px] items-center justify-center"; + +export const CSV_UPLOAD_DROP_ZONE_TEXT = + "font-normal leading-[1.5] text-[16px] text-[var(--edit-text-primary)] whitespace-nowrap"; + +export const CSV_UPLOAD_BROWSE_BTN = + "flex flex-col h-[36px] items-center justify-center overflow-clip px-[16px] py-[10px] rounded-[4px] font-semibold text-[14px] leading-[20px] text-[var(--edit-text-primary)] hover:bg-[var(--edit-tertiary-btn-hover)] active:bg-[var(--edit-tertiary-btn-pressed)] transition-colors cursor-pointer"; + +export const CSV_UPLOAD_DESCRIPTION = + "font-normal text-[14px] leading-[1.5] text-[var(--edit-text-secondary)] w-full"; + +export const CSV_UPLOAD_FILE_CHIP = + "bg-[var(--edit-container-active)] flex items-center justify-between p-[16px] rounded-[6px] w-full"; + +export const CSV_UPLOAD_FILE_CHIP_LEFT = "flex gap-[8px] items-center"; + +export const CSV_UPLOAD_FILE_CHIP_FILENAME = + "font-normal leading-[1.5] text-[16px] text-[var(--edit-text-primary)] whitespace-nowrap"; + +export const CSV_UPLOAD_FILE_CHIP_RIGHT = "flex gap-[8px] items-center"; + +export const CSV_UPLOAD_FILE_CHIP_SIZE = + "font-normal text-[14px] leading-[1.5] text-[var(--edit-text-secondary)] whitespace-nowrap"; + +export const CSV_UPLOAD_FILE_CHIP_REMOVE = + "flex items-center justify-center size-4 text-[var(--edit-text-primary)] hover:opacity-70 transition-opacity cursor-pointer"; From 9c845c4df7dccb667bb8e48909dd3f548327ca0f Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 23 May 2026 23:58:56 -0700 Subject: [PATCH 063/119] style(edit): increase width of csv upload overlay for desktop --- app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx | 4 ++-- app/ui/src/app/edit/formStyles.v2.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index c54689f0..64a0e686 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -3,13 +3,13 @@ import { useRef, useState } from "react"; import { OVERLAY_BACKDROP, - OVERLAY_PANEL, OVERLAY_HEADER, OVERLAY_TITLE, OVERLAY_CLOSE_BTN, OVERLAY_FOOTER, OVERLAY_CANCEL_BTN, OVERLAY_PRIMARY_BTN, + CSV_UPLOAD_OVERLAY_PANEL, CSV_UPLOAD_OVERLAY_INNER, CSV_UPLOAD_OVERLAY_CONTENT, CSV_UPLOAD_OVERLAY_TOP, @@ -60,7 +60,7 @@ export default function CSVUploadOverlay({ return (
e.stopPropagation()} role="dialog" aria-modal="true" diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 7d809f87..60e3982f 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -769,6 +769,9 @@ export const ERROR_POPUP_FOOTER = "flex items-center justify-end"; // ── CSV Upload Overlay (Figma 8102:1548 desktop / 7472:5765 mobile) ─────────── +export const CSV_UPLOAD_OVERLAY_PANEL = + "bg-[var(--edit-bg-primary)] flex flex-col gap-[14px] items-end overflow-hidden p-4 lg:p-6 rounded-[6px] w-full lg:max-w-[800px] mx-2 lg:mx-4 shadow-lg max-h-[90dvh]"; + export const CSV_UPLOAD_OVERLAY_INNER = "flex flex-col gap-[16px] items-end w-full"; From 9a5840d630f00270426dfa7034a19541ccc2198e Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 00:31:52 -0700 Subject: [PATCH 064/119] feat(edit): add loading state to the csvUploadOverlay --- .../components/address/AddressSection.tsx | 20 ++---- .../components/address/CSVUploadOverlay.tsx | 72 +++++++++++-------- .../edit/components/shared/SpinnerIcon.tsx | 44 ++++++++++++ app/ui/src/app/edit/formStyles.v2.ts | 11 +++ app/ui/src/app/edit/page.tsx | 16 ++++- 5 files changed, 115 insertions(+), 48 deletions(-) create mode 100644 app/ui/src/app/edit/components/shared/SpinnerIcon.tsx diff --git a/app/ui/src/app/edit/components/address/AddressSection.tsx b/app/ui/src/app/edit/components/address/AddressSection.tsx index 90f051c8..48c011ce 100644 --- a/app/ui/src/app/edit/components/address/AddressSection.tsx +++ b/app/ui/src/app/edit/components/address/AddressSection.tsx @@ -9,7 +9,6 @@ import AddressCard from "@/app/edit/components/address/AddressCard"; import ConfirmDeletionOverlay from "@/app/edit/components/shared/ConfirmDeletionOverlay"; import AddressEmptyState from "@/app/edit/components/address/AddressEmptyState"; import AddressRowHeader from "@/app/edit/components/address/AddressRowHeader"; -import CSVUploadOverlay from "@/app/edit/components/address/CSVUploadOverlay"; import type { AddressCard as AddressCardType } from "@/app/edit/types/delivery"; import { ADDRESS_EMPTY_STATE, @@ -55,7 +54,7 @@ type AddressSectionProps = { searchQuery: string; setSearchQuery: (q: string) => void; outOfRegionIds: number[]; - onOpenImportModal: (file: File) => void; + onOpenUploadOverlay: () => void; }; export default function AddressSection({ @@ -73,9 +72,8 @@ export default function AddressSection({ searchQuery, setSearchQuery, outOfRegionIds, - onOpenImportModal, + onOpenUploadOverlay, }: AddressSectionProps) { - const [isUploadOverlayOpen, setIsUploadOverlayOpen] = useState(false); const [addressToDeleteId, setAddressToDeleteId] = useState( null, ); @@ -105,7 +103,7 @@ export default function AddressSection({
- {isUploadOverlayOpen && ( - setIsUploadOverlayOpen(false)} - onFileSelect={(file) => { - onOpenImportModal(file); - setIsUploadOverlayOpen(false); - }} - /> - )} - {addressToDeleteId !== null && ( void; @@ -42,6 +43,7 @@ export default function CSVUploadOverlay({ }: CSVUploadOverlayProps) { const fileInputRef = useRef(null); const [selectedFile, setSelectedFile] = useState(null); + const [isUploading, setIsUploading] = useState(false); function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0] ?? null; @@ -54,7 +56,10 @@ export default function CSVUploadOverlay({ } function handleNext() { - if (selectedFile) onFileSelect(selectedFile); + if (selectedFile) { + setIsUploading(true); + onFileSelect(selectedFile); + } } return ( @@ -100,32 +105,38 @@ export default function CSVUploadOverlay({ {/* Drop zone */}
- + {isUploading ? ( + + ) : ( + <> + -
-

- Drag and drop CSV files here, or -

- -
+
+

+ Drag and drop CSV files here, or +

+ +
+ + )}
- Import delivery details from a CSV file. Maximum file size of X - MB. + Import delivery details from a CSV file. Maximum file size of 10 MB.

- {/* File chip — visible only when a file is selected */} - {selectedFile !== null && ( + {/* File chip — visible only when a file is selected and not uploading */} + {selectedFile !== null && !isUploading && (
Next diff --git a/app/ui/src/app/edit/components/shared/SpinnerIcon.tsx b/app/ui/src/app/edit/components/shared/SpinnerIcon.tsx new file mode 100644 index 00000000..5bd89915 --- /dev/null +++ b/app/ui/src/app/edit/components/shared/SpinnerIcon.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { + SPINNER_ICON_ARC, + SPINNER_ICON_RING, + SPINNER_ICON_WRAPPER, +} from "@/app/edit/formStyles.v2"; + +export default function SpinnerIcon() { + return ( +
+ + +
+ ); +} diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 60e3982f..1b03fc74 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -811,3 +811,14 @@ export const CSV_UPLOAD_FILE_CHIP_SIZE = export const CSV_UPLOAD_FILE_CHIP_REMOVE = "flex items-center justify-center size-4 text-[var(--edit-text-primary)] hover:opacity-70 transition-opacity cursor-pointer"; + +export const CSV_UPLOAD_UPLOADING_TEXT = + "font-normal leading-[1.5] text-[16px] text-[var(--edit-text-primary)] whitespace-nowrap"; + +// ── SpinnerIcon ─────────────────────────────────────────────────────────────── + +export const SPINNER_ICON_WRAPPER = "relative size-[33px]"; + +export const SPINNER_ICON_RING = "absolute inset-0"; + +export const SPINNER_ICON_ARC = "absolute inset-0 animate-spin"; diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index a356c6ae..ec73433a 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -30,6 +30,7 @@ import EditPageFooter from "@/app/edit/components/footer/EditPageFooter"; import MobileEditPageFooter from "@/app/edit/components/footer/MobileEditPageFooter"; import MobileBottomBar from "@/app/components/navbar/MobileBottomBar"; import { CSVImportModal } from "@/app/edit/components/address/CSVImportModal"; +import CSVUploadOverlay from "@/app/edit/components/address/CSVUploadOverlay"; import { useVehicles } from "@/app/edit/hooks/useVehicles"; import { useAddresses } from "@/app/edit/hooks/useAddresses"; import { useOptimize } from "@/app/edit/hooks/useOptimize"; @@ -82,6 +83,12 @@ export default function Page() { closeImportModal, } = useCSVImport(); + const [isUploadOverlayOpen, setIsUploadOverlayOpen] = useState(false); + + useEffect(() => { + if (isImportModalOpen || parseError) setIsUploadOverlayOpen(false); + }, [isImportModalOpen, parseError]); + useEffect(() => { let cancelled = false; @@ -175,6 +182,13 @@ export default function Page() { return (
+ {isUploadOverlayOpen && ( + setIsUploadOverlayOpen(false)} + onFileSelect={openImportModal} + /> + )} + {/* In-page import modal — stays on edit page after confirm */} {isImportModalOpen && ( setIsUploadOverlayOpen(true)} /> From 2a41157bfacf5288a5fdcea161006298ea3ce062 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 00:33:35 -0700 Subject: [PATCH 065/119] style(edit): change location of vehicle buttons --- .../components/vehicle/VehicleSection.tsx | 42 ++++++++++--------- app/ui/src/app/edit/formStyles.v2.ts | 2 + 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx index 98f2f032..ba456bcd 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx @@ -19,9 +19,10 @@ import { VEHICLE_INFO_HEADER_ROW, VEHICLE_INFO_ROWS, VEHICLE_SECTION_BTN_GHOST, - VEHICLE_SECTION_ACTIONS, + VEHICLE_SECTION_BTN_GROUP, VEHICLE_SECTION_HEADER, VEHICLE_SECTION_HEADING, + VEHICLE_SECTION_HEADING_ROW, VEHICLE_SECTION_SUBHEADING, MOBILE_EMPTY_STATE_CONTAINER, VEHICLE_MOBILE_LIST, @@ -111,25 +112,26 @@ export default function VehicleSection({

Vehicle details

-

Manage your delivery fleet

-
- -
- - +
+

Manage your delivery fleet

+
+ + +
+
{/* Desktop: card with header + vehicle rows */} diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 1b03fc74..0e055942 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -226,6 +226,8 @@ export const VEHICLE_SECTION_ACTIONS = export const VEHICLE_SECTION_HEADING_ROW = "flex items-center justify-between"; +export const VEHICLE_SECTION_BTN_GROUP = "flex items-center gap-2"; + export const VEHICLE_SECTION_OPTIMIZE_BTN = "h-9 px-[16px] rounded-[4px] bg-[var(--edit-btn-primary)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip cursor-pointer disabled:cursor-not-allowed"; From bd99c94647e45a71134f21818f370d311b790395 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 00:34:14 -0700 Subject: [PATCH 066/119] feat(edit): add file size validation for CSV import --- app/ui/src/app/edit/hooks/useCSVImport.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/ui/src/app/edit/hooks/useCSVImport.ts b/app/ui/src/app/edit/hooks/useCSVImport.ts index 8649e531..95ce3029 100644 --- a/app/ui/src/app/edit/hooks/useCSVImport.ts +++ b/app/ui/src/app/edit/hooks/useCSVImport.ts @@ -29,6 +29,13 @@ export function useCSVImport() { const [isLoading, setIsLoading] = useState(false); const openImportModal = useCallback((file: File) => { + if (file.size > 10 * 1024 * 1024) { + setParseError( + "Your file exceeds 10MB limit, please use a smaller file.", + ); + return; + } + setParseError(null); setIsLoading(true); From c0068f279cb3683e8059633ced7a8e2fc6118c12 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 00:59:50 -0700 Subject: [PATCH 067/119] fix(edit): fix location of vehicle action buttons on mobile --- app/ui/src/app/edit/formStyles.v2.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 0e055942..de88736a 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -224,9 +224,10 @@ export const VEHICLE_SECTION_BTN_GHOST = export const VEHICLE_SECTION_ACTIONS = "flex items-center justify-end gap-2 mb-4"; -export const VEHICLE_SECTION_HEADING_ROW = "flex items-center justify-between"; +export const VEHICLE_SECTION_HEADING_ROW = + "flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between"; -export const VEHICLE_SECTION_BTN_GROUP = "flex items-center gap-2"; +export const VEHICLE_SECTION_BTN_GROUP = "flex items-center gap-2 self-end lg:self-auto"; export const VEHICLE_SECTION_OPTIMIZE_BTN = "h-9 px-[16px] rounded-[4px] bg-[var(--edit-btn-primary)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap overflow-clip cursor-pointer disabled:cursor-not-allowed"; From 06fc95ead2bbe3193af7c5c4155dfecbdc53f23c Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 01:12:37 -0700 Subject: [PATCH 068/119] style(edit): upgraded optimizing modal from mid-fi to hi-fi design --- .../components/shared/OptimizingModal.tsx | 30 +++++++++---------- app/ui/src/app/edit/formStyles.v2.ts | 10 +++++++ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/app/ui/src/app/edit/components/shared/OptimizingModal.tsx b/app/ui/src/app/edit/components/shared/OptimizingModal.tsx index 1a9d5ce8..9ae6f8a9 100644 --- a/app/ui/src/app/edit/components/shared/OptimizingModal.tsx +++ b/app/ui/src/app/edit/components/shared/OptimizingModal.tsx @@ -1,14 +1,14 @@ "use client"; import { - MODAL_MESSAGE, - MODAL_OVERLAY, - MODAL_PANEL, - MODAL_TITLE, - OPTIMIZING_SPINNER, -} from "@/app/edit/formStyles"; -import { OPTIMIZING_SPINNER_WRAP } from "@/app/edit/formStyles.v2"; + OPTIMIZING_MODAL_PANEL, + OPTIMIZING_MODAL_STATUS_ROW, + OPTIMIZING_MODAL_STATUS_TEXT, + OVERLAY_BACKDROP, + OVERLAY_TITLE, +} from "@/app/edit/formStyles.v2"; import { useFocusTrap } from "@/app/edit/hooks/useFocusTrap"; +import SpinnerIcon from "@/app/edit/components/shared/SpinnerIcon"; type OptimizingModalProps = { isOpen: boolean; @@ -20,24 +20,22 @@ export default function OptimizingModal({ isOpen }: OptimizingModalProps) { if (!isOpen) return null; return ( -
+
-

- Optimizing routes… +

+ Optimizing your delivery routes

-

- This may take a few seconds. Please wait. -

-
-
diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index de88736a..510fb994 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -661,6 +661,16 @@ export const MOBILE_FOOTER_TEXT_LINE = "relative shrink-0"; export const OPTIMIZING_SPINNER_WRAP = "flex justify-center mt-2"; +// ── OptimizingModal ─────────────────────────────────────────────────────────── + +export const OPTIMIZING_MODAL_PANEL = + "bg-[var(--edit-bg-primary)] flex flex-col gap-6 overflow-hidden p-6 rounded-[6px] w-full max-w-[480px] mx-2 lg:mx-4 shadow-lg"; + +export const OPTIMIZING_MODAL_STATUS_ROW = "flex items-center gap-2 w-full"; + +export const OPTIMIZING_MODAL_STATUS_TEXT = + "font-normal text-[16px] leading-[1.5] text-[var(--edit-text-primary)] whitespace-nowrap"; + // ── Drag-Drop Overlay (Figma 8080:3134) ────────────────────────────────────── export const DRAG_DROP_OVERLAY_ROOT = From 777e03de757a9fa0d48f545aa5c846fbc7332e9a Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 01:20:38 -0700 Subject: [PATCH 069/119] fix(edit): address notes field resizing bug by measuing height of notes field at every change --- .../edit/components/address/AddressCard.tsx | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/app/ui/src/app/edit/components/address/AddressCard.tsx b/app/ui/src/app/edit/components/address/AddressCard.tsx index 8c602e5c..fb506ec7 100644 --- a/app/ui/src/app/edit/components/address/AddressCard.tsx +++ b/app/ui/src/app/edit/components/address/AddressCard.tsx @@ -205,8 +205,23 @@ function AutoResizeNotesTextarea({ const textarea = textareaRef.current; if (!textarea) return; - textarea.style.height = "auto"; - textarea.style.height = `${textarea.scrollHeight}px`; + const fitHeight = () => { + textarea.style.height = "auto"; + textarea.style.height = `${textarea.scrollHeight}px`; + }; + + fitHeight(); + + const mq = window.matchMedia("(min-width: 1024px)"); + mq.addEventListener("change", fitHeight); + + const ro = new ResizeObserver(fitHeight); + ro.observe(textarea); + + return () => { + mq.removeEventListener("change", fitHeight); + ro.disconnect(); + }; }, [value]); return ( @@ -405,7 +420,7 @@ export default function AddressCard({ ) } /> - {qtyInvalid && } + {qtyInvalid && }
{/* Delivery estimation — edit */} @@ -819,7 +834,7 @@ export default function AddressCard({ ) } /> - {qtyInvalid && } + {qtyInvalid && }
From 5bfee81e015ada10c2ff377451277ac6bb37d90a Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 11:26:03 -0700 Subject: [PATCH 070/119] refactor(edit): fully migrated from formStyles to formStylesv2 --- .../components/address/AddressSection.tsx | 11 +- app/ui/src/app/edit/formStyles.ts | 343 ------------------ app/ui/src/app/edit/formStyles.v2.ts | 14 +- 3 files changed, 13 insertions(+), 355 deletions(-) delete mode 100644 app/ui/src/app/edit/formStyles.ts diff --git a/app/ui/src/app/edit/components/address/AddressSection.tsx b/app/ui/src/app/edit/components/address/AddressSection.tsx index 48c011ce..1e24bd44 100644 --- a/app/ui/src/app/edit/components/address/AddressSection.tsx +++ b/app/ui/src/app/edit/components/address/AddressSection.tsx @@ -10,11 +10,6 @@ import ConfirmDeletionOverlay from "@/app/edit/components/shared/ConfirmDeletion import AddressEmptyState from "@/app/edit/components/address/AddressEmptyState"; import AddressRowHeader from "@/app/edit/components/address/AddressRowHeader"; import type { AddressCard as AddressCardType } from "@/app/edit/types/delivery"; -import { - ADDRESS_EMPTY_STATE, - ADDRESS_TOOLBAR_DESKTOP, -} from "@/app/edit/formStyles"; - import AddressSearchBar from "@/app/edit/components/address/AddressSearchBar"; import { ADDRESS_SECTION_HEADER, @@ -26,6 +21,8 @@ import { ADDRESS_LIST_CONTAINER_INNER, ADDRESS_LIST_DIVIDER, ADDRESS_SEARCH_DESKTOP_SIZE, + ADDRESS_SEARCH_NO_RESULTS, + ADDRESS_TOOLBAR_DESKTOP, ADDRESS_TOOLBAR_SPACER, MOBILE_EMPTY_STATE_CONTAINER, MOBILE_ADDR_TOOLBAR_ROOT, @@ -164,7 +161,7 @@ export default function AddressSection({ {addressesCount > 0 && (
{searchQuery.trim() !== "" && addressesOnCurrentPage.length === 0 ? ( -
No Addresses Found
+
No Addresses Found
) : ( addressesOnCurrentPage.map((a) => ( ) : searchQuery.trim() !== "" && addressesOnCurrentPage.length === 0 ? ( -
No Addresses Found
+
No Addresses Found
) : ( addressesOnCurrentPage.map((a) => ( Date: Sun, 24 May 2026 11:35:22 -0700 Subject: [PATCH 071/119] feat(edit): added support for dragging files in the csvUploadOverlay --- .../components/address/CSVUploadOverlay.tsx | 32 ++++++++++++++++++- app/ui/src/app/edit/edit.module.css | 4 +++ app/ui/src/app/edit/formStyles.v2.ts | 3 ++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index 6d3a0baf..22c00141 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -14,6 +14,7 @@ import { CSV_UPLOAD_OVERLAY_CONTENT, CSV_UPLOAD_OVERLAY_TOP, CSV_UPLOAD_DROP_ZONE, + CSV_UPLOAD_DROP_ZONE_ACTIVE, CSV_UPLOAD_DROP_ZONE_INNER, CSV_UPLOAD_DROP_ZONE_TEXT, CSV_UPLOAD_BROWSE_BTN, @@ -44,6 +45,7 @@ export default function CSVUploadOverlay({ const fileInputRef = useRef(null); const [selectedFile, setSelectedFile] = useState(null); const [isUploading, setIsUploading] = useState(false); + const [isDragOver, setIsDragOver] = useState(false); function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0] ?? null; @@ -62,6 +64,27 @@ export default function CSVUploadOverlay({ } } + function handleDragOver(e: React.DragEvent) { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(true); + } + + function handleDragLeave(e: React.DragEvent) { + e.preventDefault(); + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + setIsDragOver(false); + } + } + + function handleDrop(e: React.DragEvent) { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + const file = e.dataTransfer.files[0] ?? null; + if (file) setSelectedFile(file); + } + return (
{/* Drop zone */} -
+
{isUploading ? ( diff --git a/app/ui/src/app/edit/edit.module.css b/app/ui/src/app/edit/edit.module.css index 42704e0c..43142c25 100644 --- a/app/ui/src/app/edit/edit.module.css +++ b/app/ui/src/app/edit/edit.module.css @@ -81,6 +81,10 @@ /* ── Drag-drop overlay ──────────────────────────────────────── */ --edit-drag-overlay-bg: rgba(213, 242, 232, 0.48); + /* ── CSV drop zone drag-active state ────────────────────────── */ + --edit-drop-zone-active-border: #57ac91; + --edit-drop-zone-active-bg: #f0faf6; + /* Address illustration */ --edit-address-accent: #f97316; --edit-address-on-accent: #ffffff; diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index f23ab2d2..fc67600a 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -801,6 +801,9 @@ export const CSV_UPLOAD_OVERLAY_TOP = export const CSV_UPLOAD_DROP_ZONE = "bg-[var(--edit-stone-50)] border border-[var(--edit-stone-200)] border-dashed flex flex-col h-[200px] items-center justify-center overflow-clip pt-[24px] pb-[16px] rounded-[6px] w-full"; +export const CSV_UPLOAD_DROP_ZONE_ACTIVE = + "bg-[var(--edit-drop-zone-active-bg)] border border-[var(--edit-drop-zone-active-border)] border-dashed flex flex-col h-[200px] items-center justify-center overflow-clip pt-[24px] pb-[16px] rounded-[6px] w-full"; + export const CSV_UPLOAD_DROP_ZONE_INNER = "flex flex-col gap-[8px] items-center justify-center"; From 07628a2efff06d6a501a6b96f8f131fc865cd2ac Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 11:48:02 -0700 Subject: [PATCH 072/119] feat(edit): prevent users from uploading non-csv files with error handling --- .../app/edit/components/address/CSVUploadOverlay.tsx | 9 ++++++++- app/ui/src/app/edit/page.tsx | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index 22c00141..c6271de3 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -31,6 +31,7 @@ import SpinnerIcon from "@/app/edit/components/shared/SpinnerIcon"; type CSVUploadOverlayProps = { onClose: () => void; onFileSelect: (file: File) => void; + onInvalidFile?: () => void; }; function formatFileSize(bytes: number): string { @@ -41,6 +42,7 @@ function formatFileSize(bytes: number): string { export default function CSVUploadOverlay({ onClose, onFileSelect, + onInvalidFile, }: CSVUploadOverlayProps) { const fileInputRef = useRef(null); const [selectedFile, setSelectedFile] = useState(null); @@ -82,7 +84,12 @@ export default function CSVUploadOverlay({ e.stopPropagation(); setIsDragOver(false); const file = e.dataTransfer.files[0] ?? null; - if (file) setSelectedFile(file); + if (!file) return; + if (file.name.toLowerCase().endsWith(".csv")) { + setSelectedFile(file); + } else { + onInvalidFile?.(); + } } return ( diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index ec73433a..3f385305 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -53,6 +53,7 @@ export default function Page() { const vehicleState = useVehicles(); const addressState = useAddresses(); const [sessionError, setSessionError] = useState(null); + const [uploadError, setUploadError] = useState(null); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const { importVehicles } = vehicleState; const { importAddresses } = addressState; @@ -186,6 +187,12 @@ export default function Page() { setIsUploadOverlayOpen(false)} onFileSelect={openImportModal} + onInvalidFile={() => { + setIsUploadOverlayOpen(false); + setUploadError( + "This file type is not accepted. Please upload a CSV file.", + ); + }} /> )} @@ -203,6 +210,10 @@ export default function Page() { + setUploadError(null)} + /> {needsDepotAddress && ( Date: Sun, 24 May 2026 13:02:05 -0700 Subject: [PATCH 073/119] feat(edit); wire up dragging files onto edit page directly with DragDropOverlay --- .../components/address/CSVUploadOverlay.tsx | 6 ++- app/ui/src/app/edit/page.tsx | 49 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index c6271de3..35e757fb 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -32,6 +32,7 @@ type CSVUploadOverlayProps = { onClose: () => void; onFileSelect: (file: File) => void; onInvalidFile?: () => void; + initialFile?: File; }; function formatFileSize(bytes: number): string { @@ -43,9 +44,12 @@ export default function CSVUploadOverlay({ onClose, onFileSelect, onInvalidFile, + initialFile, }: CSVUploadOverlayProps) { const fileInputRef = useRef(null); - const [selectedFile, setSelectedFile] = useState(null); + const [selectedFile, setSelectedFile] = useState( + initialFile ?? null, + ); const [isUploading, setIsUploading] = useState(false); const [isDragOver, setIsDragOver] = useState(false); diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index 3f385305..492c4883 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -31,6 +31,7 @@ import MobileEditPageFooter from "@/app/edit/components/footer/MobileEditPageFoo import MobileBottomBar from "@/app/components/navbar/MobileBottomBar"; import { CSVImportModal } from "@/app/edit/components/address/CSVImportModal"; import CSVUploadOverlay from "@/app/edit/components/address/CSVUploadOverlay"; +import DragDropOverlay from "@/app/edit/components/shared/DragDropOverlay"; import { useVehicles } from "@/app/edit/hooks/useVehicles"; import { useAddresses } from "@/app/edit/hooks/useAddresses"; import { useOptimize } from "@/app/edit/hooks/useOptimize"; @@ -54,6 +55,8 @@ export default function Page() { const addressState = useAddresses(); const [sessionError, setSessionError] = useState(null); const [uploadError, setUploadError] = useState(null); + const [dragCount, setDragCount] = useState(0); + const [pendingDropFile, setPendingDropFile] = useState(null); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const { importVehicles } = vehicleState; const { importAddresses } = addressState; @@ -86,6 +89,9 @@ export default function Page() { const [isUploadOverlayOpen, setIsUploadOverlayOpen] = useState(false); + const isDraggingOverPage = + dragCount > 0 && !isUploadOverlayOpen && !isImportModalOpen; + useEffect(() => { if (isImportModalOpen || parseError) setIsUploadOverlayOpen(false); }, [isImportModalOpen, parseError]); @@ -181,6 +187,39 @@ export default function Page() { [optimize], ); + useEffect(() => { + if (!isUploadOverlayOpen) setPendingDropFile(null); + }, [isUploadOverlayOpen]); + + function handlePageDragEnter(e: React.DragEvent) { + e.preventDefault(); + setDragCount((c) => c + 1); + } + + function handlePageDragLeave(e: React.DragEvent) { + e.preventDefault(); + setDragCount((c) => Math.max(0, c - 1)); + } + + function handlePageDragOver(e: React.DragEvent) { + e.preventDefault(); + } + + function handlePageDrop(e: React.DragEvent) { + e.preventDefault(); + setDragCount(0); + const file = e.dataTransfer.files[0] ?? null; + if (!file) return; + if (file.name.toLowerCase().endsWith(".csv")) { + setPendingDropFile(file); + setIsUploadOverlayOpen(true); + } else { + setUploadError( + "This file type is not accepted. Please upload a CSV file.", + ); + } + } + return (
{isUploadOverlayOpen && ( @@ -193,6 +232,7 @@ export default function Page() { "This file type is not accepted. Please upload a CSV file.", ); }} + initialFile={pendingDropFile ?? undefined} /> )} @@ -234,7 +274,14 @@ export default function Page() { -
+
+ {isDraggingOverPage && }
void optimize()} From 1721ffaca16752feda592df58f6aacc19628a107 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:03:36 -0700 Subject: [PATCH 074/119] fix(edit): ensure DragDropOverlay covers whole body --- app/ui/src/app/edit/formStyles.v2.ts | 6 ++++-- app/ui/src/app/edit/page.tsx | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index fc67600a..0060b885 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -117,8 +117,10 @@ export const PAGE_V2_ROOT = export const PAGE_V2_BODY = "flex flex-1 lg:min-h-0 lg:overflow-hidden"; +export const PAGE_V2_MAIN_OUTER = "relative flex-1 min-w-0"; + export const PAGE_V2_MAIN = - "relative flex-1 min-w-0 bg-[var(--edit-bg-primary)] border-t-0 lg:border-t border-l-0 lg:border-l border-[var(--edit-stone-200)] rounded-none lg:rounded-tl-[12px] p-6 pb-[136px] lg:pb-6 lg:overflow-y-auto space-y-16"; + "h-full bg-[var(--edit-bg-primary)] border-t-0 lg:border-t border-l-0 lg:border-l border-[var(--edit-stone-200)] rounded-none lg:rounded-tl-[12px] p-6 pb-[136px] lg:pb-6 lg:overflow-y-auto space-y-16"; export const VEHICLE_INFO_CONTAINER = "hidden lg:flex flex-col gap-4 border border-[var(--edit-stone-200)] rounded-[8px] overflow-hidden p-4"; @@ -678,7 +680,7 @@ export const OPTIMIZING_MODAL_STATUS_TEXT = // ── Drag-Drop Overlay (Figma 8080:3134) ────────────────────────────────────── export const DRAG_DROP_OVERLAY_ROOT = - "absolute inset-0 z-50 flex items-center justify-center bg-[var(--edit-drag-overlay-bg)] border-4 border-[var(--edit-teal-600)] rounded-tl-[12px]"; + "absolute inset-0 z-50 flex items-center justify-center pointer-events-none bg-[var(--edit-drag-overlay-bg)] border-4 border-[var(--edit-teal-600)] rounded-tl-[12px]"; export const DRAG_DROP_OVERLAY_CONTENT = "flex flex-col gap-4 items-center w-[250px]"; diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index 492c4883..0bdc9677 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -17,6 +17,7 @@ import SidebarResultsButton from "@/app/components/sidebar/SidebarResultsButton" import { PAGE_V2_ROOT, PAGE_V2_BODY, + PAGE_V2_MAIN_OUTER, PAGE_V2_MAIN, ADDRESS_SECTION_WITH_PAGINATION, MANAGE_VEHICLE_GROUP, @@ -274,14 +275,15 @@ export default function Page() { -
+
{isDraggingOverPage && } +
void optimize()} @@ -305,7 +307,8 @@ export default function Page() {
-
+
+
); From 756b387912de29eabea2dc0bfdd8beb0f68c7306 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:05:53 -0700 Subject: [PATCH 075/119] style(edit): improve code formatting and readability across multiple components --- .../edit/components/address/AddressCard.tsx | 4 +- .../components/address/AddressOverlay.tsx | 8 +-- .../components/address/CSVUploadOverlay.tsx | 7 ++- .../components/shared/ManageSectionHeader.tsx | 5 +- .../components/shared/OptimizeErrorPopup.tsx | 5 +- .../components/shared/OptimizingModal.tsx | 4 +- .../vehicle/VehicleDetailsOverlay.tsx | 12 ++--- .../edit/components/vehicle/VehicleRow.tsx | 4 +- .../components/vehicle/VehicleSection.tsx | 4 +- app/ui/src/app/edit/formStyles.v2.ts | 3 +- app/ui/src/app/edit/hooks/useCSVImport.ts | 4 +- app/ui/src/app/edit/page.tsx | 53 ++++++++++--------- 12 files changed, 61 insertions(+), 52 deletions(-) diff --git a/app/ui/src/app/edit/components/address/AddressCard.tsx b/app/ui/src/app/edit/components/address/AddressCard.tsx index fb506ec7..32c047e9 100644 --- a/app/ui/src/app/edit/components/address/AddressCard.tsx +++ b/app/ui/src/app/edit/components/address/AddressCard.tsx @@ -404,7 +404,9 @@ export default function AddressCard({ min={1} max={1_000_000} ariaLabel="Delivery quantity" - onChange={(v) => updateAddress(a.id, "deliveryQuantity", v)} + onChange={(v) => + updateAddress(a.id, "deliveryQuantity", v) + } onIncrement={() => updateAddress( a.id, diff --git a/app/ui/src/app/edit/components/address/AddressOverlay.tsx b/app/ui/src/app/edit/components/address/AddressOverlay.tsx index d88d9c51..0f769bc5 100644 --- a/app/ui/src/app/edit/components/address/AddressOverlay.tsx +++ b/app/ui/src/app/edit/components/address/AddressOverlay.tsx @@ -376,9 +376,7 @@ export default function AddressOverlay({ aria-required="true" aria-invalid={zipError} /> - {zipError && ( - - )} + {zipError && }
@@ -425,9 +423,7 @@ export default function AddressOverlay({ ))}
- {countryError && ( - - )} + {countryError && }
diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index 35e757fb..be3164e3 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -139,7 +139,9 @@ export default function CSVUploadOverlay({ {/* Drop zone */}
- Import delivery details from a CSV file. Maximum file size of 10 MB. + Import delivery details from a CSV file. Maximum file size of 10 + MB.

diff --git a/app/ui/src/app/edit/components/shared/ManageSectionHeader.tsx b/app/ui/src/app/edit/components/shared/ManageSectionHeader.tsx index cdea3a5e..45cc8d81 100644 --- a/app/ui/src/app/edit/components/shared/ManageSectionHeader.tsx +++ b/app/ui/src/app/edit/components/shared/ManageSectionHeader.tsx @@ -12,7 +12,10 @@ type Props = { isOptimizing: boolean; }; -export default function ManageSectionHeader({ onOptimize, isOptimizing }: Props) { +export default function ManageSectionHeader({ + onOptimize, + isOptimizing, +}: Props) { return (

Manage

diff --git a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx index ba1baab6..e146641e 100644 --- a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx +++ b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx @@ -18,7 +18,10 @@ type OptimizeErrorPopupProps = { onClose: () => void; }; -export default function OptimizeErrorPopup({ message, onClose }: OptimizeErrorPopupProps) { +export default function OptimizeErrorPopup({ + message, + onClose, +}: OptimizeErrorPopupProps) { const panelRef = useFocusTrap(!!message); const handleKeyDown = (e: React.KeyboardEvent) => { diff --git a/app/ui/src/app/edit/components/shared/OptimizingModal.tsx b/app/ui/src/app/edit/components/shared/OptimizingModal.tsx index 9ae6f8a9..3540909a 100644 --- a/app/ui/src/app/edit/components/shared/OptimizingModal.tsx +++ b/app/ui/src/app/edit/components/shared/OptimizingModal.tsx @@ -35,7 +35,9 @@ export default function OptimizingModal({ isOpen }: OptimizingModalProps) {
-

Creating your delivery routes

+

+ Creating your delivery routes +

diff --git a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx index b45ce4bb..774d218d 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx @@ -292,9 +292,7 @@ export default function VehicleDetailsOverlay({
- {typeError && ( - - )} + {typeError && }
@@ -324,9 +322,7 @@ export default function VehicleDetailsOverlay({ aria-required="true" aria-invalid={capacityError} /> - {capacityError && ( - - )} + {capacityError && }
@@ -507,9 +503,7 @@ export default function VehicleDetailsOverlay({
- {departureError && ( - - )} + {departureError && } diff --git a/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx b/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx index 2bed40d4..2babb648 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx @@ -138,7 +138,9 @@ export default function VehicleRow({ onClick={() => updateVehicle(v.id, "available", true)} aria-pressed={v.available} className={ - v.available ? STATUS_TOGGLE_BTN_ACTIVE : STATUS_TOGGLE_BTN_INACTIVE + v.available + ? STATUS_TOGGLE_BTN_ACTIVE + : STATUS_TOGGLE_BTN_INACTIVE } > Available diff --git a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx index ba456bcd..43296d23 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleSection.tsx @@ -113,7 +113,9 @@ export default function VehicleSection({

Vehicle details

-

Manage your delivery fleet

+

+ Manage your delivery fleet +

From 2ceeaa0d06a5f50a81381d63008876ea78aa572e Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:29:32 -0700 Subject: [PATCH 076/119] style(edit); change quantity invalid error message to a minimalistic red border --- .../edit/components/address/AddressCard.tsx | 65 ++++++++++--------- app/ui/src/app/edit/formStyles.v2.ts | 3 +- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/app/ui/src/app/edit/components/address/AddressCard.tsx b/app/ui/src/app/edit/components/address/AddressCard.tsx index 32c047e9..a78c06aa 100644 --- a/app/ui/src/app/edit/components/address/AddressCard.tsx +++ b/app/ui/src/app/edit/components/address/AddressCard.tsx @@ -19,6 +19,7 @@ import { ADDRESS_ROW_ADDR_TRIGGER_TEXT, ADDRESS_ROW_ADDR_TRIGGER_PLACEHOLDER, ADDRESS_ROW_STEPPER_CONTAINER_NARROW, + ADDRESS_ROW_STEPPER_CONTAINER_NARROW_ERROR, ADDRESS_ROW_STEPPER_INPUT, ADDRESS_ROW_STEPPER_CONTROLS, ADDRESS_ROW_STEPPER_BUTTON, @@ -68,7 +69,6 @@ import { ADDRESS_CARD_MOBILE_ROOT, MOBILE_ADDR_EXPANDED_PANEL, MOBILE_ADDR_LOCKED_NOTES_CLAMP, - ADDRESS_ROW_QTY_FIELD_COL, } from "@/app/edit/formStyles.v2"; import FieldError from "@/app/edit/components/shared/FieldError"; import { @@ -118,6 +118,7 @@ function StepperInput({ min = 0, max, ariaLabel, + invalid = false, onIncrement, onDecrement, onChange, @@ -126,12 +127,19 @@ function StepperInput({ min?: number; max?: number; ariaLabel: string; + invalid?: boolean; onIncrement: () => void; onDecrement: () => void; onChange: (v: number) => void; }) { return ( -
+
@@ -398,32 +407,30 @@ export default function AddressCard({
{/* Quantity — edit */} -
- - updateAddress(a.id, "deliveryQuantity", v) - } - onIncrement={() => - updateAddress( - a.id, - "deliveryQuantity", - Math.min(1_000_000, (a.deliveryQuantity || 0) + 1), - ) - } - onDecrement={() => - updateAddress( - a.id, - "deliveryQuantity", - Math.max(1, (a.deliveryQuantity || 1) - 1), - ) - } - /> - {qtyInvalid && } -
+ + updateAddress(a.id, "deliveryQuantity", v) + } + onIncrement={() => + updateAddress( + a.id, + "deliveryQuantity", + Math.min(1_000_000, (a.deliveryQuantity || 0) + 1), + ) + } + onDecrement={() => + updateAddress( + a.id, + "deliveryQuantity", + Math.max(1, (a.deliveryQuantity || 1) - 1), + ) + } + /> {/* Delivery estimation — edit */}
@@ -818,6 +825,7 @@ export default function AddressCard({ min={1} max={1_000_000} ariaLabel="Delivery quantity" + invalid={qtyInvalid} onChange={(v) => updateAddress(a.id, "deliveryQuantity", v) } @@ -836,7 +844,6 @@ export default function AddressCard({ ) } /> - {qtyInvalid && }
diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 351388c8..bc8ed8bc 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -381,7 +381,8 @@ export const ADDRESS_ROW_STEPPER_CONTAINER = export const ADDRESS_ROW_STEPPER_CONTAINER_NARROW = `${ADDRESS_ROW_STEPPER_CONTAINER} w-[72px]`; -export const ADDRESS_ROW_QTY_FIELD_COL = "flex flex-col gap-2"; +export const ADDRESS_ROW_STEPPER_CONTAINER_NARROW_ERROR = + "border border-[var(--edit-error-border)] flex h-11 items-center justify-between px-2 py-[10px] rounded-[6px] shrink-0 w-[72px]"; export const ADDRESS_ROW_STEPPER_INPUT = "flex-1 min-w-0 bg-transparent outline-none text-[16px] leading-[1.5] text-[var(--edit-text-primary)] [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"; From 9f30318d9a3fa6462afe2723c3792975be2427a8 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:30:56 -0700 Subject: [PATCH 077/119] refactor(edit); do a try/catch when setting storage to catch imports that are too large to save --- .../app/edit/components/address/CSVImportModal.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/edit/components/address/CSVImportModal.tsx b/app/ui/src/app/edit/components/address/CSVImportModal.tsx index 8fd8d0ad..57397717 100644 --- a/app/ui/src/app/edit/components/address/CSVImportModal.tsx +++ b/app/ui/src/app/edit/components/address/CSVImportModal.tsx @@ -698,8 +698,16 @@ export function CSVImportModal({ // Store the fully-built AddressCard[] directly — no re-parsing needed. // edit/page.tsx reads "importedCards" on mount and calls importAddresses() // directly, bypassing parseAddressUpload entirely. - sessionStorage.setItem("importedCards", JSON.stringify(cards)); - router.push("/edit"); + try { + sessionStorage.setItem("importedCards", JSON.stringify(cards)); + router.push("/edit"); + } catch (e) { + if (e instanceof DOMException && e.name === "QuotaExceededError") { + alert("Import is too large to save. Please reduce the number of selected rows."); + } else { + throw e; + } + } } else if (importAddresses) { importAddresses(cards); onClose(); From f42f01e52d56d679ad17d85193d2693ba82d073a Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:31:33 -0700 Subject: [PATCH 078/119] refactor(edit): check for invalid files on handleFileChange --- .../src/app/edit/components/address/CSVUploadOverlay.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index be3164e3..fd6ea440 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -55,7 +55,12 @@ export default function CSVUploadOverlay({ function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0] ?? null; - setSelectedFile(file); + if(!file) return; + if(file.name.toLowerCase().endsWith(".csv")) { + setSelectedFile(file); + } else { + onInvalidFile?.(); + } e.target.value = ""; } From 8eb05d7b1a63ac71db0a65b04db77efbb3de0b10 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:32:35 -0700 Subject: [PATCH 079/119] fix(edit): be explicit with aria-hidden --- app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx index e146641e..3ea57cf2 100644 --- a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx +++ b/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx @@ -59,7 +59,7 @@ export default function OptimizeErrorPopup({ stroke="currentColor" strokeWidth="2" strokeLinecap="round" - aria-hidden + aria-hidden="true" > From b573db66fd71cc2bded57c6e48369665e10e2395 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:33:51 -0700 Subject: [PATCH 080/119] style(edit): ran prettier for cleaner formatting --- app/ui/src/app/edit/components/address/AddressCard.tsx | 4 +--- app/ui/src/app/edit/components/address/CSVImportModal.tsx | 4 +++- app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/ui/src/app/edit/components/address/AddressCard.tsx b/app/ui/src/app/edit/components/address/AddressCard.tsx index a78c06aa..d6ad393a 100644 --- a/app/ui/src/app/edit/components/address/AddressCard.tsx +++ b/app/ui/src/app/edit/components/address/AddressCard.tsx @@ -413,9 +413,7 @@ export default function AddressCard({ max={1_000_000} ariaLabel="Delivery quantity" invalid={qtyInvalid} - onChange={(v) => - updateAddress(a.id, "deliveryQuantity", v) - } + onChange={(v) => updateAddress(a.id, "deliveryQuantity", v)} onIncrement={() => updateAddress( a.id, diff --git a/app/ui/src/app/edit/components/address/CSVImportModal.tsx b/app/ui/src/app/edit/components/address/CSVImportModal.tsx index 57397717..51a27789 100644 --- a/app/ui/src/app/edit/components/address/CSVImportModal.tsx +++ b/app/ui/src/app/edit/components/address/CSVImportModal.tsx @@ -703,7 +703,9 @@ export function CSVImportModal({ router.push("/edit"); } catch (e) { if (e instanceof DOMException && e.name === "QuotaExceededError") { - alert("Import is too large to save. Please reduce the number of selected rows."); + alert( + "Import is too large to save. Please reduce the number of selected rows.", + ); } else { throw e; } diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index fd6ea440..3bec2733 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -55,8 +55,8 @@ export default function CSVUploadOverlay({ function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0] ?? null; - if(!file) return; - if(file.name.toLowerCase().endsWith(".csv")) { + if (!file) return; + if (file.name.toLowerCase().endsWith(".csv")) { setSelectedFile(file); } else { onInvalidFile?.(); From ded3059496f2260c5663dc5f108966f8d9c68123 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 13:45:57 -0700 Subject: [PATCH 081/119] fix(edit): address incorrect aria-disabled string issue --- app/ui/src/app/components/sidebar/MobileSidebar.tsx | 2 +- .../src/app/components/sidebar/SidebarResultsButton.tsx | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/components/sidebar/MobileSidebar.tsx b/app/ui/src/app/components/sidebar/MobileSidebar.tsx index 0353eb0d..95f2ed03 100644 --- a/app/ui/src/app/components/sidebar/MobileSidebar.tsx +++ b/app/ui/src/app/components/sidebar/MobileSidebar.tsx @@ -134,7 +134,7 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) { diff --git a/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx b/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx index 79c8fd49..1511c464 100644 --- a/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx +++ b/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx @@ -59,7 +59,14 @@ export default function SidebarResultsButton() { } return ( - + + {" "} + {/* TODO: add results page link when at least one route exists */} {SIDEBAR_RESULTS_ICON} Results From 64767ff129e455f162361002a65abc14da866453 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sun, 24 May 2026 14:04:09 -0700 Subject: [PATCH 082/119] fix(edit): remove alert and use proper error handling in CSVImportModal --- .../app/edit/components/address/CSVImportModal.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/edit/components/address/CSVImportModal.tsx b/app/ui/src/app/edit/components/address/CSVImportModal.tsx index 51a27789..427faff9 100644 --- a/app/ui/src/app/edit/components/address/CSVImportModal.tsx +++ b/app/ui/src/app/edit/components/address/CSVImportModal.tsx @@ -19,6 +19,7 @@ import { useState, useCallback, useMemo } from "react"; import { useRouter } from "next/navigation"; import type { AddressCard } from "@/app/edit/types/delivery"; +import OptimizeErrorPopup from "@/app/edit/components/shared/OptimizeErrorPopup"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -649,6 +650,7 @@ export function CSVImportModal({ }: CSVImportModalProps) { const router = useRouter(); const [step, setStep] = useState<1 | 2>(1); + const [errorMessage, setErrorMessage] = useState(null); const headers = useMemo(() => csvData[0] ?? [], [csvData]); const dataRows = useMemo( @@ -703,11 +705,12 @@ export function CSVImportModal({ router.push("/edit"); } catch (e) { if (e instanceof DOMException && e.name === "QuotaExceededError") { - alert( + setErrorMessage( "Import is too large to save. Please reduce the number of selected rows.", ); } else { - throw e; + console.error(e); + setErrorMessage("Something went wrong"); } } } else if (importAddresses) { @@ -728,6 +731,10 @@ export function CSVImportModal({ return ( <> + setErrorMessage(null)} + /> {step === 1 ? ( Date: Sun, 24 May 2026 14:07:46 -0700 Subject: [PATCH 083/119] refactor(edit): rename OptimizeErrorPopup to ErrorOverlay and update related components --- .../edit/components/address/CSVImportModal.tsx | 4 ++-- ...OptimizeErrorPopup.tsx => ErrorOverlay.tsx} | 18 +++++++++--------- app/ui/src/app/edit/formStyles.v2.ts | 6 +++--- app/ui/src/app/edit/page.tsx | 10 +++++----- 4 files changed, 19 insertions(+), 19 deletions(-) rename app/ui/src/app/edit/components/shared/{OptimizeErrorPopup.tsx => ErrorOverlay.tsx} (82%) diff --git a/app/ui/src/app/edit/components/address/CSVImportModal.tsx b/app/ui/src/app/edit/components/address/CSVImportModal.tsx index 427faff9..438d0a0d 100644 --- a/app/ui/src/app/edit/components/address/CSVImportModal.tsx +++ b/app/ui/src/app/edit/components/address/CSVImportModal.tsx @@ -19,7 +19,7 @@ import { useState, useCallback, useMemo } from "react"; import { useRouter } from "next/navigation"; import type { AddressCard } from "@/app/edit/types/delivery"; -import OptimizeErrorPopup from "@/app/edit/components/shared/OptimizeErrorPopup"; +import ErrorOverlay from "@/app/edit/components/shared/ErrorOverlay"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -731,7 +731,7 @@ export function CSVImportModal({ return ( <> - setErrorMessage(null)} /> diff --git a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx b/app/ui/src/app/edit/components/shared/ErrorOverlay.tsx similarity index 82% rename from app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx rename to app/ui/src/app/edit/components/shared/ErrorOverlay.tsx index 3ea57cf2..e9705433 100644 --- a/app/ui/src/app/edit/components/shared/OptimizeErrorPopup.tsx +++ b/app/ui/src/app/edit/components/shared/ErrorOverlay.tsx @@ -1,8 +1,8 @@ "use client"; import { - ERROR_POPUP_FOOTER, - ERROR_POPUP_MESSAGE, + ERROR_OVERLAY_FOOTER, + ERROR_OVERLAY_MESSAGE, OVERLAY_BACKDROP, OVERLAY_CLOSE_BTN, OVERLAY_HEADER, @@ -13,15 +13,15 @@ import { import styles from "@/app/edit/edit.module.css"; import { useFocusTrap } from "@/app/edit/hooks/useFocusTrap"; -type OptimizeErrorPopupProps = { +type ErrorOverlayProps = { message: string | null; onClose: () => void; }; -export default function OptimizeErrorPopup({ +export default function ErrorOverlay({ message, onClose, -}: OptimizeErrorPopupProps) { +}: ErrorOverlayProps) { const panelRef = useFocusTrap(!!message); const handleKeyDown = (e: React.KeyboardEvent) => { @@ -37,12 +37,12 @@ export default function OptimizeErrorPopup({ className={OVERLAY_BACKDROP} role="dialog" aria-modal="true" - aria-labelledby="error-popup-title" + aria-labelledby="error-overlay-title" onKeyDown={handleKeyDown} >
-

+

Something went wrong

-

{message}

-
+

{message}

+
-

Tap to toggle

From 33a49499a87acfbc63dcc1816570e48472c0d36f Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 14:41:26 -0700 Subject: [PATCH 089/119] refactor(edit): break one css component into two components for clarity --- app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx | 3 ++- app/ui/src/app/edit/formStyles.v2.ts | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index 889b43ab..85b4f53b 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -16,6 +16,7 @@ import { CSV_UPLOAD_DROP_ZONE, CSV_UPLOAD_DROP_ZONE_ACTIVE, CSV_UPLOAD_DROP_ZONE_INNER, + CSV_UPLOAD_DROP_ZONE_PROMPT, CSV_UPLOAD_DROP_ZONE_TEXT, CSV_UPLOAD_BROWSE_BTN, CSV_UPLOAD_DESCRIPTION, @@ -176,7 +177,7 @@ export default function CSVUploadOverlay({ /> -
+

Drag and drop CSV files here, or

diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 3b8e2a7b..541f82d3 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -811,6 +811,9 @@ export const CSV_UPLOAD_DROP_ZONE_ACTIVE = export const CSV_UPLOAD_DROP_ZONE_INNER = "flex flex-col gap-[8px] items-center justify-center"; +export const CSV_UPLOAD_DROP_ZONE_PROMPT = + "flex flex-col gap-[8px] items-center justify-center"; + export const CSV_UPLOAD_DROP_ZONE_TEXT = "font-normal leading-[1.5] text-[16px] text-[var(--edit-text-primary)] whitespace-nowrap"; From d83cc00682e6e2bd8dc344651a454505d0347b89 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 14:44:35 -0700 Subject: [PATCH 090/119] chore(edit): remove unused css components --- app/ui/src/app/edit/formStyles.v2.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 541f82d3..deda8ad0 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -221,9 +221,6 @@ export const VEHICLE_MOBILE_LOCKED_DEPARTURE = export const VEHICLE_SECTION_BTN_GHOST = "h-9 px-4 rounded-[80px] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-tertiary-btn-hover)] active:bg-[var(--edit-tertiary-btn-pressed)] transition-colors cursor-pointer"; -export const VEHICLE_SECTION_ACTIONS = - "flex items-center justify-end gap-2 mb-4"; - export const VEHICLE_SECTION_HEADING_ROW = "flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between"; @@ -667,8 +664,6 @@ export const MOBILE_FOOTER_TEXT_WRAPPER = export const MOBILE_FOOTER_TEXT_LINE = "relative shrink-0"; -export const OPTIMIZING_SPINNER_WRAP = "flex justify-center mt-2"; - // ── OptimizingModal ─────────────────────────────────────────────────────────── export const OPTIMIZING_MODAL_PANEL = @@ -839,9 +834,6 @@ export const CSV_UPLOAD_FILE_CHIP_SIZE = export const CSV_UPLOAD_FILE_CHIP_REMOVE = "flex items-center justify-center size-4 text-[var(--edit-text-primary)] hover:opacity-70 transition-opacity cursor-pointer"; -export const CSV_UPLOAD_UPLOADING_TEXT = - "font-normal leading-[1.5] text-[16px] text-[var(--edit-text-primary)] whitespace-nowrap"; - // ── SpinnerIcon ─────────────────────────────────────────────────────────────── export const SPINNER_ICON_WRAPPER = "relative size-[33px]"; From 0116ebcbee72af6e9371b2bb3ff7bf7c4e67b691 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 14:48:21 -0700 Subject: [PATCH 091/119] chore(edit): remove another unused component --- .../src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx | 1 - app/ui/src/app/edit/formStyles.v2.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx index ee5a5d62..d3cada58 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx @@ -36,7 +36,6 @@ import { OVERLAY_SELECT_ICON, OVERLAY_SELECT_WRAPPER, OVERLAY_SELECT_WRAPPER_ERROR, - OVERLAY_STATUS_HINT, OVERLAY_STATUS_ROW, STATUS_TOGGLE_WRAPPER, STATUS_TOGGLE_BTN_ACTIVE, diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index deda8ad0..68d142f9 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -517,9 +517,6 @@ export const OVERLAY_STATUS_BADGE_TEXT_AVAILABLE = export const OVERLAY_STATUS_BADGE_TEXT_IN_USE = "font-semibold text-[16px] leading-[22px] text-[var(--edit-text-secondary)] whitespace-nowrap"; -export const OVERLAY_STATUS_HINT = - "text-[12px] leading-normal text-[var(--edit-text-secondary)]"; - export const OVERLAY_STATUS_ROW = "flex gap-2 items-center"; export const OVERLAY_DEPARTURE_WRAPPER = From 7e7a5d8aedd613820c413ce03c2c30b173f84fe5 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 14:54:47 -0700 Subject: [PATCH 092/119] style(edit): updated departureTime field design to new hi-fi design --- .../vehicle/VehicleDetailsOverlay.tsx | 99 ++++++++++--------- app/ui/src/app/edit/formStyles.v2.ts | 12 ++- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx index d3cada58..bd421a42 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleDetailsOverlay.tsx @@ -13,6 +13,7 @@ import { OVERLAY_BODY, OVERLAY_CANCEL_BTN, OVERLAY_CLOSE_BTN, + OVERLAY_DEPARTURE_ROW, OVERLAY_DEPARTURE_WRAPPER, OVERLAY_DEPARTURE_WRAPPER_ERROR, OVERLAY_TIME_COLON, @@ -421,54 +422,56 @@ export default function VehicleDetailsOverlay({ * -
-
- { - const val = e.target.value - .replace(/\D/g, "") - .slice(0, 2); - setHours(val); - if (val.length === 2) minutesRef.current?.focus(); - }} - placeholder="HH" - maxLength={2} - inputMode="numeric" - className={OVERLAY_TIME_SEGMENT_INPUT} - aria-required="true" - aria-label="Departure hours" - aria-invalid={departureError} - /> - : - { - const val = e.target.value - .replace(/\D/g, "") - .slice(0, 2); - setMinutes(val); - }} - onKeyDown={(e) => { - if (e.key === "Backspace" && minutes === "") - hoursRef.current?.focus(); - }} - placeholder="MM" - maxLength={2} - inputMode="numeric" - className={OVERLAY_TIME_SEGMENT_INPUT} - aria-label="Departure minutes" - aria-invalid={departureError} - /> +
+
+
+ { + const val = e.target.value + .replace(/\D/g, "") + .slice(0, 2); + setHours(val); + if (val.length === 2) minutesRef.current?.focus(); + }} + placeholder="HH" + maxLength={2} + inputMode="numeric" + className={OVERLAY_TIME_SEGMENT_INPUT} + aria-required="true" + aria-label="Departure hours" + aria-invalid={departureError} + /> + : + { + const val = e.target.value + .replace(/\D/g, "") + .slice(0, 2); + setMinutes(val); + }} + onKeyDown={(e) => { + if (e.key === "Backspace" && minutes === "") + hoursRef.current?.focus(); + }} + placeholder="MM" + maxLength={2} + inputMode="numeric" + className={OVERLAY_TIME_SEGMENT_INPUT} + aria-label="Departure minutes" + aria-invalid={departureError} + /> +
Date: Wed, 27 May 2026 23:28:39 -0700 Subject: [PATCH 093/119] fix(edit): Add "" fallback to prevent undefined trimmedDepotAddress from throwing --- app/ui/src/app/edit/hooks/useOptimize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/hooks/useOptimize.ts b/app/ui/src/app/edit/hooks/useOptimize.ts index 91ebcba4..7c53bdb9 100644 --- a/app/ui/src/app/edit/hooks/useOptimize.ts +++ b/app/ui/src/app/edit/hooks/useOptimize.ts @@ -362,7 +362,7 @@ export function useOptimize( resultBody as VroomResponse, lockedVehicles, addresses, - trimmedDepotAddress!, + trimmedDepotAddress || "", ); setOptimizeResults(routes); router.push("/results"); From 1d930434d6d3b49c6549791a88e8a51f9146b7c3 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Wed, 27 May 2026 23:42:50 -0700 Subject: [PATCH 094/119] chore(edit): prevent drift risk in ADDRESS_ROW_STEPPER_CONTAINER tailwind components --- app/ui/src/app/edit/formStyles.v2.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 2f44248b..1bdec9e4 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -378,8 +378,8 @@ export const ADDRESS_ROW_STEPPER_CONTAINER = export const ADDRESS_ROW_STEPPER_CONTAINER_NARROW = `${ADDRESS_ROW_STEPPER_CONTAINER} w-[72px]`; -export const ADDRESS_ROW_STEPPER_CONTAINER_NARROW_ERROR = - "border border-[var(--edit-error-border)] flex h-11 items-center justify-between px-2 py-[10px] rounded-[6px] shrink-0 w-[72px]"; +export const ADDRESS_ROW_STEPPER_CONTAINER_NARROW_ERROR = + `${ADDRESS_ROW_STEPPER_CONTAINER_NARROW} border-[var(--edit-error-border)]`; export const ADDRESS_ROW_STEPPER_INPUT = "flex-1 min-w-0 bg-transparent outline-none text-[16px] leading-[1.5] text-[var(--edit-text-primary)] [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"; From a826fb283e3218f0a46a5795847849b3a472416d Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Wed, 27 May 2026 23:44:56 -0700 Subject: [PATCH 095/119] fix(edit): address bad import and incorrect ending tag in SidebarResultsButton --- app/ui/src/app/components/sidebar/SidebarResultsButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx b/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx index 1511c464..6cf293dd 100644 --- a/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx +++ b/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; -import { useHasOptimizeResults } from "../../../utils/hasOptimizeResults"; +import { useHasOptimizeResults } from "@/app/edit/utils/hasOptimizeResults"; import { SIDEBAR_NAV_ITEM_ACTIVE, SIDEBAR_NAV_ITEM_DISABLED, @@ -69,6 +69,6 @@ export default function SidebarResultsButton() { {/* TODO: add results page link when at least one route exists */} {SIDEBAR_RESULTS_ICON} Results - + ); } From acc1da813548ef79745ae990190a25655b90127c Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Thu, 28 May 2026 00:06:47 -0700 Subject: [PATCH 096/119] style(results): ensure visual consistency between AdvancedMarker and Marker --- app/ui/src/app/results/components/Map.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/ui/src/app/results/components/Map.tsx b/app/ui/src/app/results/components/Map.tsx index a90b96c8..bb7d7464 100644 --- a/app/ui/src/app/results/components/Map.tsx +++ b/app/ui/src/app/results/components/Map.tsx @@ -281,6 +281,10 @@ function stopKey(vehicleId: string, stopId: string): string { return `${vehicleId}:${stopId}`; } +const DEPOT_MARKER_SVG = `data:image/svg+xml;charset=UTF-8,${encodeURIComponent( + `S`, +)}`; + function AdvancedMarkers({ map, routes, @@ -317,10 +321,11 @@ function AdvancedMarkers({ routes.forEach((route) => { // Depot marker — distinct non-draggable pin labeled "S" if (route.startLocation) { - const depotEl = document.createElement("div"); - depotEl.style.cssText = - "display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;background:#374151;color:#fff;font-size:11px;font-weight:700;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,0.4)"; - depotEl.textContent = "S"; + const depotEl = document.createElement("img"); + depotEl.src = DEPOT_MARKER_SVG; + depotEl.width = 28; + depotEl.height = 28; + depotEl.alt = ""; const depotMarker = new AdvancedMarkerElement({ map, position: { @@ -513,7 +518,11 @@ export default function MapComponent({ }} title={route.startLocation.address || "Starting point"} draggable={false} - label={{ text: "S", color: "#fff", fontWeight: "bold" }} + icon={{ + url: DEPOT_MARKER_SVG, + scaledSize: new google.maps.Size(28, 28), + anchor: new google.maps.Point(14, 14), + }} /> )} {sorted.map((stop) => { From b167263d78e652d27cdcf554a52e07124109cdef Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Thu, 28 May 2026 00:13:22 -0700 Subject: [PATCH 097/119] style(results): better aligned starting point styling to match the rest of the sidebar --- app/ui/src/app/results/components/Sidebar.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/ui/src/app/results/components/Sidebar.tsx b/app/ui/src/app/results/components/Sidebar.tsx index 5c377465..8c60b6a6 100644 --- a/app/ui/src/app/results/components/Sidebar.tsx +++ b/app/ui/src/app/results/components/Sidebar.tsx @@ -175,17 +175,19 @@ export default function Sidebar({
    {route.startLocation && (
  • -
    - +
    +
    + S - -
    -

    - Starting point -

    -

    - {route.startLocation.address || "Depot"} -

    + +
    +

    + Starting Point +

    +

    + {route.startLocation.address || "Depot"} +

    +
  • From 5314af30a703840c2b0ca464cfc7f07ca6f26116 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Thu, 28 May 2026 00:18:42 -0700 Subject: [PATCH 098/119] fix(edit): do file size check earlier to prevent user's from attaching files larger than 10MB limit --- .../components/address/CSVUploadOverlay.tsx | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index 85b4f53b..f2c49987 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -26,6 +26,7 @@ import { CSV_UPLOAD_FILE_CHIP_RIGHT, CSV_UPLOAD_FILE_CHIP_SIZE, CSV_UPLOAD_FILE_CHIP_REMOVE, + CSV_UPLOAD_SIZE_ERROR, } from "@/app/edit/formStyles.v2"; import SpinnerIcon from "@/app/edit/components/shared/SpinnerIcon"; @@ -36,6 +37,8 @@ type CSVUploadOverlayProps = { initialFile?: File; }; +const MAX_CSV_BYTES = 10 * 1024 * 1024; + function formatFileSize(bytes: number): string { if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; @@ -53,6 +56,7 @@ export default function CSVUploadOverlay({ ); const [isUploading, setIsUploading] = useState(false); const [isDragOver, setIsDragOver] = useState(false); + const [fileSizeError, setFileSizeError] = useState(null); function handleClose() { setIsUploading(false); @@ -62,16 +66,21 @@ export default function CSVUploadOverlay({ function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0] ?? null; if (!file) return; - if (file.name.toLowerCase().endsWith(".csv")) { - setSelectedFile(file); - } else { + if (!file.name.toLowerCase().endsWith(".csv")) { onInvalidFile?.(); + } else if (file.size > MAX_CSV_BYTES) { + setFileSizeError("Your file exceeds 10 MB. Please use a smaller file."); + setSelectedFile(null); + } else { + setFileSizeError(null); + setSelectedFile(file); } e.target.value = ""; } function handleRemoveFile() { setSelectedFile(null); + setFileSizeError(null); } function handleNext() { @@ -100,10 +109,14 @@ export default function CSVUploadOverlay({ setIsDragOver(false); const file = e.dataTransfer.files[0] ?? null; if (!file) return; - if (file.name.toLowerCase().endsWith(".csv")) { - setSelectedFile(file); - } else { + if (!file.name.toLowerCase().endsWith(".csv")) { onInvalidFile?.(); + } else if (file.size > MAX_CSV_BYTES) { + setFileSizeError("Your file exceeds 10 MB. Please use a smaller file."); + setSelectedFile(null); + } else { + setFileSizeError(null); + setSelectedFile(file); } } @@ -208,6 +221,15 @@ export default function CSVUploadOverlay({ Import delivery details from a CSV file. Maximum file size of 10 MB.

    + {fileSizeError !== null && ( +

    + {fileSizeError} +

    + )}
{/* File chip — visible only when a file is selected and not uploading */} From 78f459c9ed7675baa782fbdb206f890f9d6b4a8e Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Thu, 28 May 2026 00:19:58 -0700 Subject: [PATCH 099/119] fix(edit): add missing error message style for CSV upload size validation --- app/ui/src/app/edit/formStyles.v2.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 1bdec9e4..5a436d8c 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -817,6 +817,9 @@ export const CSV_UPLOAD_BROWSE_BTN = export const CSV_UPLOAD_DESCRIPTION = "font-normal text-[14px] leading-[1.5] text-[var(--edit-text-secondary)] w-full"; +export const CSV_UPLOAD_SIZE_ERROR = + "text-sm text-[var(--edit-error-border)] mt-1"; + export const CSV_UPLOAD_FILE_CHIP = "bg-[var(--edit-container-active)] flex items-center justify-between p-[16px] rounded-[6px] w-full"; From 8d906fa20213f7174c92a902ad841c9b1c736596 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Thu, 28 May 2026 00:50:02 -0700 Subject: [PATCH 100/119] style: improve formatting in edit and results folders --- app/ui/src/app/edit/formStyles.v2.ts | 3 +-- app/ui/src/app/results/components/Sidebar.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index 5a436d8c..dfa98e08 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -378,8 +378,7 @@ export const ADDRESS_ROW_STEPPER_CONTAINER = export const ADDRESS_ROW_STEPPER_CONTAINER_NARROW = `${ADDRESS_ROW_STEPPER_CONTAINER} w-[72px]`; -export const ADDRESS_ROW_STEPPER_CONTAINER_NARROW_ERROR = - `${ADDRESS_ROW_STEPPER_CONTAINER_NARROW} border-[var(--edit-error-border)]`; +export const ADDRESS_ROW_STEPPER_CONTAINER_NARROW_ERROR = `${ADDRESS_ROW_STEPPER_CONTAINER_NARROW} border-[var(--edit-error-border)]`; export const ADDRESS_ROW_STEPPER_INPUT = "flex-1 min-w-0 bg-transparent outline-none text-[16px] leading-[1.5] text-[var(--edit-text-primary)] [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"; diff --git a/app/ui/src/app/results/components/Sidebar.tsx b/app/ui/src/app/results/components/Sidebar.tsx index 8c60b6a6..189c3b7b 100644 --- a/app/ui/src/app/results/components/Sidebar.tsx +++ b/app/ui/src/app/results/components/Sidebar.tsx @@ -178,7 +178,7 @@ export default function Sidebar({
- S + S

From ed1af282745d80d676885c4194d2a378ba04e7a7 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 14:25:02 -0700 Subject: [PATCH 101/119] fix(results): correct bad import paths --- app/ui/src/app/results/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/ui/src/app/results/page.tsx b/app/ui/src/app/results/page.tsx index cf388613..4e2f4a1c 100644 --- a/app/ui/src/app/results/page.tsx +++ b/app/ui/src/app/results/page.tsx @@ -5,9 +5,9 @@ import { useCallback, useEffect, useState } from "react"; import { NAVBAR_V2_LOGO, NAVBAR_V2_ROOT } from "../edit/formStyles.v2"; import styles from "../edit/edit.module.css"; -import EditSidebar from "../edit/components/layout/sidebar/Sidebar"; -import SidebarEditButton from "../edit/components/layout/sidebar/SidebarEditButton"; -import SidebarResultsButton from "../edit/components/layout/sidebar/SidebarResultsButton"; +import EditSidebar from "@/app/components/sidebar/Sidebar"; +import SidebarEditButton from "@/app/components/sidebar/SidebarEditButton"; +import SidebarResultsButton from "@/app/components/sidebar/SidebarResultsButton"; import MapComponent from "./components/Map"; import Sidebar from "./components/Sidebar"; import type { PendingPinMove, Route } from "./types"; From 8c9b911a39b181686d37743b52dfa5a895ae1a0e Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 14:35:53 -0700 Subject: [PATCH 102/119] fix(edit): re-add optimize button in the MobileBottomBar. --- .../app/components/navbar/MobileBottomBar.tsx | 18 +++++++++++++++++- app/ui/src/app/edit/formStyles.v2.ts | 2 +- app/ui/src/app/edit/page.tsx | 6 +++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/ui/src/app/components/navbar/MobileBottomBar.tsx b/app/ui/src/app/components/navbar/MobileBottomBar.tsx index a51e18f2..19059839 100644 --- a/app/ui/src/app/components/navbar/MobileBottomBar.tsx +++ b/app/ui/src/app/components/navbar/MobileBottomBar.tsx @@ -1,16 +1,21 @@ +import styles from "@/app/edit/edit.module.css"; import { MOBILE_BOTTOM_BAR_ROOT, MOBILE_BOTTOM_BAR_INNER, MOBILE_BOTTOM_BAR_ACTIONS_ROW, + MOBILE_BOTTOM_BAR_OPTIMIZE_BTN, + MOBILE_BOTTOM_BAR_OPTIMIZE_LABEL, MOBILE_BOTTOM_BAR_SECONDARY_BTN, MOBILE_BOTTOM_BAR_SECONDARY_LABEL, } from "@/app/edit/formStyles.v2"; type Props = { onSave: () => void; + onOptimize: () => void; + isOptimizing: boolean; }; -export default function MobileBottomBar({ onSave }: Props) { +export default function MobileBottomBar({ onSave, onOptimize, isOptimizing }: Props) { return (

@@ -22,6 +27,17 @@ export default function MobileBottomBar({ onSave }: Props) { > Save +
diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index dfa98e08..efb9efff 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -760,7 +760,7 @@ export const MOBILE_BOTTOM_BAR_ROOT = export const MOBILE_BOTTOM_BAR_INNER = "flex flex-col gap-[8px] w-full"; export const MOBILE_BOTTOM_BAR_OPTIMIZE_BTN = - "flex items-center justify-center h-[44px] px-[16px] py-[10px] rounded-[80px] w-full overflow-clip bg-[var(--edit-btn-primary)] cursor-pointer"; + "flex items-center justify-center h-[44px] px-[16px] py-[10px] rounded-[80px] flex-1 min-w-0 overflow-clip bg-[var(--edit-btn-primary)] cursor-pointer disabled:cursor-not-allowed"; export const MOBILE_BOTTOM_BAR_OPTIMIZE_LABEL = "font-['Manrope',sans-serif] font-semibold text-[16px] leading-[22px] text-[var(--edit-text-primary)] whitespace-nowrap"; diff --git a/app/ui/src/app/edit/page.tsx b/app/ui/src/app/edit/page.tsx index e6313c6c..79de2fbf 100644 --- a/app/ui/src/app/edit/page.tsx +++ b/app/ui/src/app/edit/page.tsx @@ -267,7 +267,11 @@ export default function Page() { isOpen={isMobileMenuOpen} onClose={() => setIsMobileMenuOpen(false)} /> - + void optimize()} + isOptimizing={isOptimizing} + /> setIsMobileMenuOpen(true)} />
From 77e4fd0d6be98a2fd4558ee97e7600a9790b745c Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 14:43:18 -0700 Subject: [PATCH 103/119] style(edit): prettier formatting --- app/ui/src/app/components/navbar/MobileBottomBar.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/ui/src/app/components/navbar/MobileBottomBar.tsx b/app/ui/src/app/components/navbar/MobileBottomBar.tsx index 19059839..40f3f2f2 100644 --- a/app/ui/src/app/components/navbar/MobileBottomBar.tsx +++ b/app/ui/src/app/components/navbar/MobileBottomBar.tsx @@ -15,7 +15,11 @@ type Props = { isOptimizing: boolean; }; -export default function MobileBottomBar({ onSave, onOptimize, isOptimizing }: Props) { +export default function MobileBottomBar({ + onSave, + onOptimize, + isOptimizing, +}: Props) { return (
From fc8e730fb06489f55551a4a51d52831e131f4a31 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 00:30:55 -0700 Subject: [PATCH 104/119] ci: improved E2E Docker workflow for full backend testing --- .github/workflows/e2e-docker.yml | 117 +++++++++++++++++++++++++++++++ .github/workflows/pr-light.yml | 13 ---- 2 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/e2e-docker.yml diff --git a/.github/workflows/e2e-docker.yml b/.github/workflows/e2e-docker.yml new file mode 100644 index 00000000..6ecfb997 --- /dev/null +++ b/.github/workflows/e2e-docker.yml @@ -0,0 +1,117 @@ +# Full backend Docker E2E suite. +# Runs every test registered with `LABELS e2e` in tests/CMakeLists.txt +# (see `deliveryoptimizer_add_docker_*e2e_test` helpers). Each E2E test brings +# up the full Compose stack (postgres + osrm + http-server) under a unique +# project name, exercises a real routing/job/proxy/health flow, and tears the +# stack down on exit. +# +# Triggers: +# - schedule: weekly, Mondays 06:00 UTC (cron: '0 6 * * 1') +# - workflow_dispatch: manual run (Actions tab -> Run workflow, or +# `gh workflow run e2e-docker.yml`) +# +# Not wired to pull_request on purpose. PR CI (pr-light.yml) keeps excluding +# e2e/docker via `ctest -LE 'e2e|docker'` because each E2E run can take a long +# time on cold caches (Docker build of OSRM + http-server image, OSRM PBF +# preprocess for Monaco, then 7 serial bring-up/tear-down cycles). +# +# Runner: native ARM64 to match deploy/compose/docker-compose.arm64.yml and +# tests/integration/e2e/e2e_stack.env (DELIVERYOPTIMIZER_PLATFORM=linux/arm64). +# If `ubuntu-24.04-arm` is unavailable on this org/repo, switch `runs-on` to +# `ubuntu-latest` and add a Buildx + QEMU setup step before the ctest call; +# emulated arm64 builds will be noticeably slower. +# +# Disabled by CMake (DISABLED TRUE): DeliveryOptimizerRoutingSmokeTest.OsrmAndVroom. +# CTest skips disabled tests automatically, so `ctest -L e2e` is safe to use. + +name: E2E Docker (weekly + manual) + +on: + schedule: + - cron: '0 6 * * 1' + workflow_dispatch: + +concurrency: + group: e2e-docker-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e: + name: ctest -L e2e (full suite) + runs-on: ubuntu-24.04-arm + timeout-minutes: 240 + steps: + - uses: actions/checkout@v4 + + - name: Install build + test prerequisites + uses: awalsh128/cache-apt-pkgs-action@v1 + with: + packages: ninja-build libjsoncpp-dev libdrogon-dev uuid-dev libmariadb-dev libbrotli-dev libhiredis-dev libyaml-cpp-dev libgtest-dev python3 + version: 1.0 + + - name: Verify toolchain + run: | + cmake --version + docker --version + docker compose version + python3 --version + bash --version | head -n 1 + + - name: Validate compose config with E2E env + run: | + docker compose \ + -f deploy/compose/docker-compose.arm64.yml \ + --env-file tests/integration/e2e/e2e_stack.env \ + config > /dev/null + + - name: Configure (CMake dev preset) + run: cmake --preset dev + + - name: Build (CMake dev preset) + run: cmake --build --preset dev + + - name: List discovered E2E tests + run: ctest --preset dev -N -L e2e + + - name: Run full E2E suite + run: ctest --preset dev --output-on-failure -L e2e + + - name: Capture Docker diagnostics on failure + if: failure() + run: | + mkdir -p e2e-diagnostics + { + echo "=== docker version ===" + docker version || true + echo + echo "=== docker ps -a ===" + docker ps -a || true + echo + echo "=== docker network ls ===" + docker network ls || true + echo + echo "=== docker volume ls ===" + docker volume ls || true + echo + echo "=== docker images (deliveryoptimizer*) ===" + docker images "deliveryoptimizer*" || true + } > e2e-diagnostics/docker-state.txt 2>&1 + + # Each E2E test uses its own unique compose project name and cleans + # up on EXIT, so logs are usually unavailable post-mortem. Attempt a + # best-effort dump for any leftover containers. + for cid in $(docker ps -aq); do + ( + echo "=== docker logs ${cid} ===" + docker logs "${cid}" 2>&1 || true + ) >> e2e-diagnostics/leftover-container-logs.txt + done + + - name: Upload diagnostics artifact + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-docker-diagnostics + path: e2e-diagnostics/ + retention-days: 7 + if-no-files-found: ignore \ No newline at end of file diff --git a/.github/workflows/pr-light.yml b/.github/workflows/pr-light.yml index 556e7dbd..34f748ec 100644 --- a/.github/workflows/pr-light.yml +++ b/.github/workflows/pr-light.yml @@ -63,16 +63,3 @@ jobs: - run: cmake --build --preset dev --parallel - run: ctest --preset dev --output-on-failure --no-tests=error -LE 'e2e|docker' - run: docker compose -f deploy/compose/docker-compose.arm64.yml --env-file deploy/env/http-server.arm64.env config - backend-e2e: - if: github.event_name != 'pull_request' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: docker/setup-qemu-action@v3 - - uses: awalsh128/cache-apt-pkgs-action@v1 - with: - packages: ninja-build libjsoncpp-dev libdrogon-dev uuid-dev libmariadb-dev libbrotli-dev libhiredis-dev libyaml-cpp-dev libgtest-dev postgresql postgresql-client - version: 1.0 - - run: cmake --preset dev - - run: cmake --build --preset dev --parallel - - run: ctest --preset dev --output-on-failure --no-tests=error -L 'e2e|docker' From 5d24a0f9488933246ab3a5b193b72c0d6b5284bb Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 00:49:25 -0700 Subject: [PATCH 105/119] ci: created new playwright e2e frontend check --- .github/workflows/playwright-e2e.yml | 112 ++++++++++++++++++ app/ui/package-lock.json | 80 ++++++++++--- app/ui/package.json | 4 +- app/ui/playwright.config.ts | 18 +++ app/ui/tests/e2e/optimize-flow.spec.ts | 84 +++++++++++++ .../compose/docker-compose.e2e-override.yml | 8 ++ tests/integration/e2e/e2e_sf_playwright.env | 28 +++++ 7 files changed, 317 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/playwright-e2e.yml create mode 100644 app/ui/playwright.config.ts create mode 100644 app/ui/tests/e2e/optimize-flow.spec.ts create mode 100644 deploy/compose/docker-compose.e2e-override.yml create mode 100644 tests/integration/e2e/e2e_sf_playwright.env diff --git a/.github/workflows/playwright-e2e.yml b/.github/workflows/playwright-e2e.yml new file mode 100644 index 00000000..d147f4ef --- /dev/null +++ b/.github/workflows/playwright-e2e.yml @@ -0,0 +1,112 @@ +# Playwright browser E2E suite. +# Spins up the full Docker Compose backend stack (OSRM + VROOM + Postgres + C++ API), +# builds the Next.js production server, and runs a Playwright test that exercises +# the landing → edit → optimize → results flow end-to-end. +# +# OSRM routing data for San Francisco is cached between runs via actions/cache so +# PBF downloading and processing (~5-8 min) only happens on first run or when +# e2e_sf_playwright.env changes. +# +# No GitHub secrets required. Google Maps is omitted; the map panel logs a console +# error but the route sidebar (what the test asserts) renders independently. +# +# Triggers: +# - schedule: weekly, Mondays 06:00 UTC +# - workflow_dispatch: manual run (Actions tab or `gh workflow run playwright-e2e.yml`) + +name: Playwright E2E (weekly + manual) + +on: + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +concurrency: + group: playwright-e2e-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + frontend-e2e: + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + + - name: Restore OSRM cache + uses: actions/cache@v4 + with: + path: /tmp/osrm-sf-data + key: osrm-sf-${{ hashFiles('tests/integration/e2e/e2e_sf_playwright.env') }} + + - name: Prepare OSRM cache directory + run: mkdir -p /tmp/osrm-sf-data + + - name: Start backend stack + run: | + OSRM_CACHE_DIR=/tmp/osrm-sf-data \ + docker compose \ + -f deploy/compose/docker-compose.arm64.yml \ + -f deploy/compose/docker-compose.e2e-override.yml \ + --env-file tests/integration/e2e/e2e_sf_playwright.env \ + up -d --build + + # 120 × 10 s = 20 min ceiling. + # Cold run: OSRM downloads + processes the SF PBF (~5-8 min). + # Cache hit: files already exist, entrypoint skips processing, starts in <1 min. + - name: Wait for backend health + run: | + for i in $(seq 1 120); do + RESP=$(curl -fsS http://127.0.0.1:39080/health 2>/dev/null || true) + if echo "$RESP" | grep -q '"status":"ok"'; then + echo "Healthy after $((i * 10))s"; exit 0 + fi + echo "Attempt $i/120 — waiting 10s..."; sleep 10 + done + docker compose \ + -f deploy/compose/docker-compose.arm64.yml \ + -f deploy/compose/docker-compose.e2e-override.yml \ + --env-file tests/integration/e2e/e2e_sf_playwright.env \ + logs >&2 || true + exit 1 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: app/ui/package-lock.json + + - run: npm ci + working-directory: app/ui + + - run: npx playwright install --with-deps chromium + working-directory: app/ui + + - name: Build Next.js + run: npm run build + working-directory: app/ui + env: + DELIVERYOPTIMIZER_API_URL: http://127.0.0.1:39080 + + - name: Start Next.js server + run: npm start & + working-directory: app/ui + env: + DELIVERYOPTIMIZER_API_URL: http://127.0.0.1:39080 + + - run: npx wait-on http://localhost:3000 --timeout 30000 + working-directory: app/ui + + - name: Run Playwright E2E tests + run: npm run test:e2e + working-directory: app/ui + env: + PLAYWRIGHT_BASE_URL: http://localhost:3000 + CI: "true" + + - name: Upload Playwright report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: app/ui/playwright-report/ + retention-days: 7 diff --git a/app/ui/package-lock.json b/app/ui/package-lock.json index 1c38af88..296b649d 100644 --- a/app/ui/package-lock.json +++ b/app/ui/package-lock.json @@ -18,6 +18,7 @@ "zod": "^4.3.6" }, "devDependencies": { + "@playwright/test": "^1.49.1", "@tailwindcss/postcss": "^4", "@types/google.maps": "^3.58.1", "@types/node": "^20.19.35", @@ -1266,6 +1267,22 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@react-google-maps/api": { "version": "2.20.8", "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.20.8.tgz", @@ -5975,30 +5992,61 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.4" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "node": ">= 0.4" } }, "node_modules/postcss": { diff --git a/app/ui/package.json b/app/ui/package.json index a7fcebf9..1e5dc4dc 100644 --- a/app/ui/package.json +++ b/app/ui/package.json @@ -10,6 +10,7 @@ "typecheck": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", + "test:e2e": "playwright test", "format": "prettier --write .", "format:check": "prettier --check ." }, @@ -40,6 +41,7 @@ "prettier": "^3.8.3", "tailwindcss": "^4", "typescript": "^5", - "vitest": "^4.1.4" + "vitest": "^4.1.4", + "@playwright/test": "^1.49.1" } } diff --git a/app/ui/playwright.config.ts b/app/ui/playwright.config.ts new file mode 100644 index 00000000..52d68d6d --- /dev/null +++ b/app/ui/playwright.config.ts @@ -0,0 +1,18 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests/e2e", + timeout: 120_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: [["html"], ["github"]], + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3000", + trace: "on-first-retry", + video: "on-first-retry", + }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], +}); diff --git a/app/ui/tests/e2e/optimize-flow.spec.ts b/app/ui/tests/e2e/optimize-flow.spec.ts new file mode 100644 index 00000000..983c8dfd --- /dev/null +++ b/app/ui/tests/e2e/optimize-flow.spec.ts @@ -0,0 +1,84 @@ +import { test, expect, type Page } from "@playwright/test"; + +const NOMINATIM_MOCK = [ + { + lat: "37.7749", + lon: "-122.4194", + address: { state: "California", country_code: "us" }, + }, +]; + +async function fillAddressOverlay( + page: Page, + dialogName: string, + primaryLabel: string, +) { + const overlay = page.getByRole("dialog", { name: dialogName }); + await overlay.waitFor(); + await overlay.locator("#start-loc-line1").fill("100 Market St"); + await overlay.locator("#start-loc-city").fill("San Francisco"); + await overlay.locator("#start-loc-state").selectOption("California"); + await overlay.locator("#start-loc-zip").fill("94105"); + await overlay.locator("#start-loc-country").selectOption("United States"); + await overlay.getByRole("button", { name: primaryLabel }).click(); +} + +async function addDeliveryAddress(page: Page, recipientName: string) { + await page.getByRole("button", { name: "Add address" }).click(); + await page.locator('[aria-label="Recipient name"]').first().fill(recipientName); + await page.locator('[aria-label="Edit recipient address"]').first().click(); + await fillAddressOverlay(page, "Enter Address", "Confirm"); + await page.locator('[aria-label="Confirm row"]').first().click(); +} + +test("optimize flow routes 2 stops to 1 vehicle", async ({ page }) => { + test.setTimeout(180_000); + + await page.route(/nominatim\.openstreetmap\.org\/search/, (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(NOMINATIM_MOCK), + }), + ); + + // Landing → Welcome → Edit + await page.goto("/"); + await page.getByRole("button", { name: "Route manager — continue" }).click(); + await page.waitForURL("**/welcome"); + await page.getByRole("button", { name: "New user — continue" }).click(); + await page.waitForURL("**/edit"); + + // Add one vehicle via overlay + await page.getByRole("button", { name: "Add vehicle" }).click(); + const vehicleDialog = page.getByRole("dialog", { name: "Add vehicle details" }); + await vehicleDialog.waitFor(); + await vehicleDialog.locator("#overlay-vehicle-name").fill("E2E Van"); + await vehicleDialog.locator("#overlay-vehicle-type").selectOption("truck"); + await vehicleDialog.locator("#overlay-vehicle-capacity").fill("100"); + await vehicleDialog.locator("#overlay-vehicle-unit").selectOption("units"); + await vehicleDialog.locator('[aria-label="Departure hours"]').fill("08"); + await vehicleDialog.locator('[aria-label="Departure minutes"]').fill("00"); + await vehicleDialog.getByRole("button", { name: "Done" }).click(); + + // Add two delivery addresses + await addDeliveryAddress(page, "Stop One"); + await addDeliveryAddress(page, "Stop Two"); + + // Click Optimize in the navbar header (scoped to avoid colliding with depot overlay button) + await page.getByRole("banner").getByRole("button", { name: "Optimize" }).click(); + + // Fill depot address overlay (appears because no start location is set) + await fillAddressOverlay( + page, + "Enter starting location for all driver routes", + "Optimize", + ); + + // Assert optimization succeeded by confirming route data in the sidebar + await page.waitForURL("**/results", { timeout: 120_000 }); + await expect( + page.getByRole("heading", { name: "Optimized Routes" }), + ).toBeVisible(); + await expect(page.getByText("1 route with 2 total stops")).toBeVisible(); +}); diff --git a/deploy/compose/docker-compose.e2e-override.yml b/deploy/compose/docker-compose.e2e-override.yml new file mode 100644 index 00000000..6632b1f5 --- /dev/null +++ b/deploy/compose/docker-compose.e2e-override.yml @@ -0,0 +1,8 @@ +name: deliveryoptimizer + +services: + osrm: + volumes: + - ${OSRM_CACHE_DIR}:/osrm-cache + environment: + OSRM_DATA_DIR: /osrm-cache diff --git a/tests/integration/e2e/e2e_sf_playwright.env b/tests/integration/e2e/e2e_sf_playwright.env new file mode 100644 index 00000000..8200fc35 --- /dev/null +++ b/tests/integration/e2e/e2e_sf_playwright.env @@ -0,0 +1,28 @@ +DELIVERYOPTIMIZER_PLATFORM=linux/amd64 +DELIVERYOPTIMIZER_IMAGE=deliveryoptimizer-http:amd64 +DELIVERYOPTIMIZER_OSRM_IMAGE=deliveryoptimizer-osrm:amd64 +DELIVERYOPTIMIZER_POSTGRES_IMAGE=postgres:16-alpine +DELIVERYOPTIMIZER_HOST_PORT=39080 +DELIVERYOPTIMIZER_PG_DSN=host=postgres port=5432 dbname=deliveryoptimizer user=deliveryoptimizer password=deliveryoptimizer +DELIVERYOPTIMIZER_JOB_DB_CONNECTIONS=4 +DELIVERYOPTIMIZER_JOB_WORKERS=2 +DELIVERYOPTIMIZER_JOB_POLL_MS=250 +DELIVERYOPTIMIZER_JOB_HEARTBEAT_MS=1000 +DELIVERYOPTIMIZER_JOB_SWEEP_MS=1000 +DELIVERYOPTIMIZER_JOB_LEASE_MS=90000 +DELIVERYOPTIMIZER_JOB_RESULT_TTL_SECONDS=86400 +DELIVERYOPTIMIZER_JOB_WORKER_HEALTH_MS=5000 +POSTGRES_DB=deliveryoptimizer +POSTGRES_USER=deliveryoptimizer +POSTGRES_PASSWORD=deliveryoptimizer +UBUNTU_VERSION=24.04 +CONAN_VERSION=2.18.1 +OSRM_REF=v5.27.1 +OSRM_BUILD_JOBS=2 +OSRM_UBUNTU_VERSION=22.04 +OSRM_PBF_URL=https://download.bbbike.org/osm/bbbike/SanFrancisco/SanFrancisco.osm.pbf +OSRM_PROFILE=/opt/osrm-backend/profiles/car.lua +OSRM_INTERNAL_PORT=5001 +VROOM_REF=v1.14.0 +VROOM_BUILD_JOBS=2 +VROOM_TIMEOUT_SECONDS=30 From 4603acd79fb2a542a72aa720fa4199b86a38011e Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 15:30:47 -0700 Subject: [PATCH 106/119] ci: address mismatch between monaco build in e2e-docker and san francisco health check by using env variables to use monach health check in e2e-docker ci --- .github/workflows/e2e-docker.yml | 2 +- deploy/compose/docker-compose.arm64.yml | 2 +- tests/integration/e2e/e2e_sf_playwright.env | 2 ++ tests/integration/e2e/e2e_stack.env | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-docker.yml b/.github/workflows/e2e-docker.yml index 6ecfb997..31b3d9e3 100644 --- a/.github/workflows/e2e-docker.yml +++ b/.github/workflows/e2e-docker.yml @@ -13,7 +13,7 @@ # Not wired to pull_request on purpose. PR CI (pr-light.yml) keeps excluding # e2e/docker via `ctest -LE 'e2e|docker'` because each E2E run can take a long # time on cold caches (Docker build of OSRM + http-server image, OSRM PBF -# preprocess for Monaco, then 7 serial bring-up/tear-down cycles). +# preprocess for Monaco, then 8 serial bring-up/tear-down cycles). # # Runner: native ARM64 to match deploy/compose/docker-compose.arm64.yml and # tests/integration/e2e/e2e_stack.env (DELIVERYOPTIMIZER_PLATFORM=linux/arm64). diff --git a/deploy/compose/docker-compose.arm64.yml b/deploy/compose/docker-compose.arm64.yml index 0b16db45..fe32cafe 100644 --- a/deploy/compose/docker-compose.arm64.yml +++ b/deploy/compose/docker-compose.arm64.yml @@ -43,7 +43,7 @@ services: test: [ "CMD-SHELL", - "curl -fsS \"http://127.0.0.1:${OSRM_INTERNAL_PORT:-5001}/nearest/v1/driving/-122.4194,37.7749?number=1&generate_hints=false\" | grep -q '\"code\":\"Ok\"'", + "curl -fsS \"http://127.0.0.1:${OSRM_INTERNAL_PORT:-5001}/nearest/v1/driving/${OSRM_HEALTHCHECK_LON:--122.4194},${OSRM_HEALTHCHECK_LAT:-37.7749}?number=1&generate_hints=false\" | grep -q '\"code\":\"Ok\"'", ] interval: 30s timeout: 10s diff --git a/tests/integration/e2e/e2e_sf_playwright.env b/tests/integration/e2e/e2e_sf_playwright.env index 8200fc35..353c851e 100644 --- a/tests/integration/e2e/e2e_sf_playwright.env +++ b/tests/integration/e2e/e2e_sf_playwright.env @@ -21,6 +21,8 @@ OSRM_REF=v5.27.1 OSRM_BUILD_JOBS=2 OSRM_UBUNTU_VERSION=22.04 OSRM_PBF_URL=https://download.bbbike.org/osm/bbbike/SanFrancisco/SanFrancisco.osm.pbf +OSRM_HEALTHCHECK_LON=-122.4194 +OSRM_HEALTHCHECK_LAT=37.7749 OSRM_PROFILE=/opt/osrm-backend/profiles/car.lua OSRM_INTERNAL_PORT=5001 VROOM_REF=v1.14.0 diff --git a/tests/integration/e2e/e2e_stack.env b/tests/integration/e2e/e2e_stack.env index a7229e12..fb335bcd 100644 --- a/tests/integration/e2e/e2e_stack.env +++ b/tests/integration/e2e/e2e_stack.env @@ -21,6 +21,8 @@ OSRM_REF=v5.27.1 OSRM_BUILD_JOBS=2 OSRM_UBUNTU_VERSION=22.04 OSRM_PBF_URL=https://download.geofabrik.de/europe/monaco-latest.osm.pbf +OSRM_HEALTHCHECK_LON=7.4236 +OSRM_HEALTHCHECK_LAT=43.7384 OSRM_PROFILE=/opt/osrm-backend/profiles/car.lua OSRM_INTERNAL_PORT=5001 VROOM_REF=v1.14.0 From 8ec6f604182e7a7e892ead7ffa94066cf75ce535 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 15:38:55 -0700 Subject: [PATCH 107/119] style(edit): prettier formatting --- app/ui/tests/e2e/optimize-flow.spec.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/ui/tests/e2e/optimize-flow.spec.ts b/app/ui/tests/e2e/optimize-flow.spec.ts index 983c8dfd..1a99c39b 100644 --- a/app/ui/tests/e2e/optimize-flow.spec.ts +++ b/app/ui/tests/e2e/optimize-flow.spec.ts @@ -25,7 +25,10 @@ async function fillAddressOverlay( async function addDeliveryAddress(page: Page, recipientName: string) { await page.getByRole("button", { name: "Add address" }).click(); - await page.locator('[aria-label="Recipient name"]').first().fill(recipientName); + await page + .locator('[aria-label="Recipient name"]') + .first() + .fill(recipientName); await page.locator('[aria-label="Edit recipient address"]').first().click(); await fillAddressOverlay(page, "Enter Address", "Confirm"); await page.locator('[aria-label="Confirm row"]').first().click(); @@ -51,7 +54,9 @@ test("optimize flow routes 2 stops to 1 vehicle", async ({ page }) => { // Add one vehicle via overlay await page.getByRole("button", { name: "Add vehicle" }).click(); - const vehicleDialog = page.getByRole("dialog", { name: "Add vehicle details" }); + const vehicleDialog = page.getByRole("dialog", { + name: "Add vehicle details", + }); await vehicleDialog.waitFor(); await vehicleDialog.locator("#overlay-vehicle-name").fill("E2E Van"); await vehicleDialog.locator("#overlay-vehicle-type").selectOption("truck"); @@ -66,7 +71,10 @@ test("optimize flow routes 2 stops to 1 vehicle", async ({ page }) => { await addDeliveryAddress(page, "Stop Two"); // Click Optimize in the navbar header (scoped to avoid colliding with depot overlay button) - await page.getByRole("banner").getByRole("button", { name: "Optimize" }).click(); + await page + .getByRole("banner") + .getByRole("button", { name: "Optimize" }) + .click(); // Fill depot address overlay (appears because no start location is set) await fillAddressOverlay( From 3e8da239da167d00b62e4e01e420026586a86a49 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 15:46:55 -0700 Subject: [PATCH 108/119] chore(edit): add .cursor/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1a6a98ca..77eb2449 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,4 @@ services/vroom/data/ # Claude Claude.md .claude/ +.cursor/ \ No newline at end of file From db24cea1b0eb5c4d3a2799fcedee6761530ce649 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Mon, 25 May 2026 15:50:56 -0700 Subject: [PATCH 109/119] chore(edit): exclusde e2e test from vitest and minor comment change --- .gitignore | 2 +- app/ui/vitest.config.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 77eb2449..cb0accfa 100644 --- a/.gitignore +++ b/.gitignore @@ -92,7 +92,7 @@ services/vroom/data/ # macOS / editor noise .DS_Store -# Claude +# Claude and Cursor Claude.md .claude/ .cursor/ \ No newline at end of file diff --git a/app/ui/vitest.config.ts b/app/ui/vitest.config.ts index f47e8478..c411c8a4 100644 --- a/app/ui/vitest.config.ts +++ b/app/ui/vitest.config.ts @@ -4,6 +4,8 @@ import path from "path"; export default defineConfig({ test: { environment: "node", + include: ["src/**/*.test.ts"], + exclude: ["tests/e2e/**", "node_modules/**"], }, resolve: { alias: { From adf8578898c37000977a0c08467840048a87509b Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 15:36:11 -0700 Subject: [PATCH 110/119] test: address UI inconsistencies in playwright test --- app/ui/tests/e2e/optimize-flow.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/ui/tests/e2e/optimize-flow.spec.ts b/app/ui/tests/e2e/optimize-flow.spec.ts index 1a99c39b..e1546f31 100644 --- a/app/ui/tests/e2e/optimize-flow.spec.ts +++ b/app/ui/tests/e2e/optimize-flow.spec.ts @@ -64,15 +64,15 @@ test("optimize flow routes 2 stops to 1 vehicle", async ({ page }) => { await vehicleDialog.locator("#overlay-vehicle-unit").selectOption("units"); await vehicleDialog.locator('[aria-label="Departure hours"]').fill("08"); await vehicleDialog.locator('[aria-label="Departure minutes"]').fill("00"); - await vehicleDialog.getByRole("button", { name: "Done" }).click(); + await vehicleDialog.getByRole("button", { name: "Confirm" }).click(); // Add two delivery addresses await addDeliveryAddress(page, "Stop One"); await addDeliveryAddress(page, "Stop Two"); - // Click Optimize in the navbar header (scoped to avoid colliding with depot overlay button) + // Optimize button is in ManageSectionHeader inside
, not the navbar await page - .getByRole("banner") + .getByRole("main") .getByRole("button", { name: "Optimize" }) .click(); From 58a1fea81bd1154d85dfa3a7320d95967ff157e1 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 15:44:25 -0700 Subject: [PATCH 111/119] ci: allow cmake to build multiple files in parallel in e2e-docker check --- .github/workflows/e2e-docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-docker.yml b/.github/workflows/e2e-docker.yml index 31b3d9e3..e061922c 100644 --- a/.github/workflows/e2e-docker.yml +++ b/.github/workflows/e2e-docker.yml @@ -68,7 +68,7 @@ jobs: run: cmake --preset dev - name: Build (CMake dev preset) - run: cmake --build --preset dev + run: cmake --build --preset dev --parallel - name: List discovered E2E tests run: ctest --preset dev -N -L e2e @@ -114,4 +114,4 @@ jobs: name: e2e-docker-diagnostics path: e2e-diagnostics/ retention-days: 7 - if-no-files-found: ignore \ No newline at end of file + if-no-files-found: ignore From 2be830892453ee95d97c98640f92d8d17a9eb307 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 15:58:47 -0700 Subject: [PATCH 112/119] refactor(edit): consolidated duplicate code for available/in-use by creating StatusToggle --- .../edit/components/vehicle/StatusToggle.tsx | 37 +++++++++ .../edit/components/vehicle/VehicleRow.tsx | 75 +++---------------- 2 files changed, 48 insertions(+), 64 deletions(-) create mode 100644 app/ui/src/app/edit/components/vehicle/StatusToggle.tsx diff --git a/app/ui/src/app/edit/components/vehicle/StatusToggle.tsx b/app/ui/src/app/edit/components/vehicle/StatusToggle.tsx new file mode 100644 index 00000000..1ebbd05d --- /dev/null +++ b/app/ui/src/app/edit/components/vehicle/StatusToggle.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { + STATUS_TOGGLE_WRAPPER, + STATUS_TOGGLE_BTN_ACTIVE, + STATUS_TOGGLE_BTN_INACTIVE, + STATUS_TOGGLE_TEXT, +} from "@/app/edit/formStyles.v2"; + +type Props = { + vehicleId: number; + available: boolean; + onUpdate: (id: number, field: "available", value: boolean) => void; +}; + +export default function StatusToggle({ vehicleId, available, onUpdate }: Props) { + return ( +
+ + +
+ ); +} diff --git a/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx b/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx index 2babb648..c8fa1fed 100644 --- a/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx +++ b/app/ui/src/app/edit/components/vehicle/VehicleRow.tsx @@ -12,15 +12,12 @@ import type { VehicleType, } from "@/app/edit/types/delivery"; import { capitalize } from "@/app/edit/utils/deliveryHelpers"; +import StatusToggle from "@/app/edit/components/vehicle/StatusToggle"; import { VEHICLE_ROW_CELL, VEHICLE_ROW_ACTIONS, VEHICLE_ROW_DESKTOP, VEHICLE_ROW_STATUS_CELL, - STATUS_TOGGLE_WRAPPER, - STATUS_TOGGLE_BTN_ACTIVE, - STATUS_TOGGLE_BTN_INACTIVE, - STATUS_TOGGLE_TEXT, VEHICLE_MOBILE_LOCKED_CARD_V2, VEHICLE_MOBILE_LOCKED_HEADER, VEHICLE_MOBILE_LOCKED_INFO, @@ -80,36 +77,11 @@ export default function VehicleRow({
-
- - -
+ {(v.departureTime || "--:--") + " departure time"} @@ -128,36 +100,11 @@ export default function VehicleRow({ {formatCapacity(v)} -
- - -
+
{v.departureTime}
From 02cbf3f1140481120648b4aa1b3c920ae471e820 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 15:59:13 -0700 Subject: [PATCH 113/119] ci: add stdout and stderr logs for npm start --- .github/workflows/playwright-e2e.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright-e2e.yml b/.github/workflows/playwright-e2e.yml index d147f4ef..4994fe05 100644 --- a/.github/workflows/playwright-e2e.yml +++ b/.github/workflows/playwright-e2e.yml @@ -88,7 +88,7 @@ jobs: DELIVERYOPTIMIZER_API_URL: http://127.0.0.1:39080 - name: Start Next.js server - run: npm start & + run: npm start > /tmp/nextjs-server.log 2>&1 & working-directory: app/ui env: DELIVERYOPTIMIZER_API_URL: http://127.0.0.1:39080 @@ -108,5 +108,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: playwright-report - path: app/ui/playwright-report/ + path: | + app/ui/playwright-report/ + /tmp/nextjs-server.log retention-days: 7 From 28e5e6aff4b7ad28f855a0fcc36ded0729b60587 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 16:00:35 -0700 Subject: [PATCH 114/119] fix(edit): ensure file exceeding limit error message is consistent --- app/ui/src/app/edit/hooks/useCSVImport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/src/app/edit/hooks/useCSVImport.ts b/app/ui/src/app/edit/hooks/useCSVImport.ts index 14b2ef8c..4d24a42f 100644 --- a/app/ui/src/app/edit/hooks/useCSVImport.ts +++ b/app/ui/src/app/edit/hooks/useCSVImport.ts @@ -30,7 +30,7 @@ export function useCSVImport() { const openImportModal = useCallback((file: File) => { if (file.size > 10 * 1024 * 1024) { - setParseError("Your file exceeds 10MB limit, please use a smaller file."); + setParseError("Your file exceeds 10 MB. Please use a smaller file."); return; } From 6ea6019f47b51fc6291b10468e89df9f66d35ea1 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 16:02:12 -0700 Subject: [PATCH 115/119] fix(edit): apply aria-dialog to the element representing the modal content, instead of the backdrop --- .../app/edit/components/shared/ErrorOverlay.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/ui/src/app/edit/components/shared/ErrorOverlay.tsx b/app/ui/src/app/edit/components/shared/ErrorOverlay.tsx index 9ca85dc1..aedeefbf 100644 --- a/app/ui/src/app/edit/components/shared/ErrorOverlay.tsx +++ b/app/ui/src/app/edit/components/shared/ErrorOverlay.tsx @@ -30,14 +30,14 @@ export default function ErrorOverlay({ message, onClose }: ErrorOverlayProps) { if (!message) return null; return ( -
-
+
+

Something went wrong From 7501962e655b4c107e826a9f3160347d32791555 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 16:23:10 -0700 Subject: [PATCH 116/119] style(edit): prettier formatting --- .../edit/components/vehicle/StatusToggle.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/app/ui/src/app/edit/components/vehicle/StatusToggle.tsx b/app/ui/src/app/edit/components/vehicle/StatusToggle.tsx index 1ebbd05d..f00432a6 100644 --- a/app/ui/src/app/edit/components/vehicle/StatusToggle.tsx +++ b/app/ui/src/app/edit/components/vehicle/StatusToggle.tsx @@ -13,14 +13,24 @@ type Props = { onUpdate: (id: number, field: "available", value: boolean) => void; }; -export default function StatusToggle({ vehicleId, available, onUpdate }: Props) { +export default function StatusToggle({ + vehicleId, + available, + onUpdate, +}: Props) { return ( -
+
@@ -28,7 +38,9 @@ export default function StatusToggle({ vehicleId, available, onUpdate }: Props) type="button" onClick={() => onUpdate(vehicleId, "available", false)} aria-pressed={available === false} - className={!available ? STATUS_TOGGLE_BTN_ACTIVE : STATUS_TOGGLE_BTN_INACTIVE} + className={ + !available ? STATUS_TOGGLE_BTN_ACTIVE : STATUS_TOGGLE_BTN_INACTIVE + } > In use From fd7ff184be3c270dfa2264adb1170c17f0ea9573 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 17:05:55 -0700 Subject: [PATCH 117/119] fix(results): add guard to prevent loading Google component for starting point marker without the script being fully loaded --- app/ui/src/app/results/components/Map.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/ui/src/app/results/components/Map.tsx b/app/ui/src/app/results/components/Map.tsx index bb7d7464..57b10831 100644 --- a/app/ui/src/app/results/components/Map.tsx +++ b/app/ui/src/app/results/components/Map.tsx @@ -518,11 +518,15 @@ export default function MapComponent({ }} title={route.startLocation.address || "Starting point"} draggable={false} - icon={{ - url: DEPOT_MARKER_SVG, - scaledSize: new google.maps.Size(28, 28), - anchor: new google.maps.Point(14, 14), - }} + icon={ + typeof google !== "undefined" + ? { + url: DEPOT_MARKER_SVG, + scaledSize: new google.maps.Size(28, 28), + anchor: new google.maps.Point(14, 14), + } + : undefined + } /> )} {sorted.map((stop) => { From b2bd27d506f681af69f2b41122071f87e847124c Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 17:33:13 -0700 Subject: [PATCH 118/119] style: fix SidebarResultsButton visual color appearence --- app/ui/src/app/components/sidebar/SidebarResultsButton.tsx | 4 ++-- app/ui/src/app/edit/formStyles.v2.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx b/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx index 6cf293dd..6c124be9 100644 --- a/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx +++ b/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx @@ -24,7 +24,7 @@ const SIDEBAR_RESULTS_ICON = ( > ); @@ -38,7 +38,7 @@ export default function SidebarResultsButton() { return ( {SIDEBAR_RESULTS_ICON} diff --git a/app/ui/src/app/edit/formStyles.v2.ts b/app/ui/src/app/edit/formStyles.v2.ts index efb9efff..ac98d892 100644 --- a/app/ui/src/app/edit/formStyles.v2.ts +++ b/app/ui/src/app/edit/formStyles.v2.ts @@ -18,7 +18,7 @@ export const SIDEBAR_NAV_ITEM_DISABLED = "flex flex-col gap-1 items-center w-full opacity-[0.26] cursor-not-allowed"; export const SIDEBAR_NAV_PILL_ACTIVE = - "w-full flex items-center justify-center rounded-[80px] bg-[var(--edit-container-active)] px-[9px] py-[4px]"; + "w-full flex items-center justify-center rounded-[80px] bg-[var(--edit-container-active)] px-[9px] py-[4px] text-[var(--edit-text-primary)]"; export const SIDEBAR_NAV_PILL_INACTIVE = "w-full flex items-center justify-center rounded-[80px] px-[9px] py-[4px]"; From cd4826f676918c9d289b8f9aa48c443fc90517d0 Mon Sep 17 00:00:00 2001 From: Guritfak Gill Date: Sat, 30 May 2026 17:35:54 -0700 Subject: [PATCH 119/119] style: prettier formatting --- app/ui/src/app/components/sidebar/SidebarResultsButton.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx b/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx index 6c124be9..c082229c 100644 --- a/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx +++ b/app/ui/src/app/components/sidebar/SidebarResultsButton.tsx @@ -37,11 +37,7 @@ export default function SidebarResultsButton() { if (isResultsPage) { return ( - - {SIDEBAR_RESULTS_ICON} - + {SIDEBAR_RESULTS_ICON} Results );