From 34fbf9e03e43bc9671cdfe301638b36e6326708c Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Wed, 24 Jun 2026 23:59:59 -0700 Subject: [PATCH] fix(driver): show invalid route upload errors on upload page --- app/ui/src/app/driver_assist/page.tsx | 94 +++++-------------- app/ui/src/app/driver_assist/styles.ts | 29 ------ app/ui/src/app/upload-route/page.tsx | 16 +++- .../app/upload-route/routeUploadValidation.ts | 9 ++ app/ui/src/tests/driverRouteImport.test.ts | 13 ++- 5 files changed, 56 insertions(+), 105 deletions(-) create mode 100644 app/ui/src/app/upload-route/routeUploadValidation.ts diff --git a/app/ui/src/app/driver_assist/page.tsx b/app/ui/src/app/driver_assist/page.tsx index 7b5228c12..e9fc95e00 100644 --- a/app/ui/src/app/driver_assist/page.tsx +++ b/app/ui/src/app/driver_assist/page.tsx @@ -3,10 +3,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; -import { - loadSessionFromFile, - loadSessionFromText, -} from "@/lib/driver-route/importSession"; +import { loadSessionFromText } from "@/lib/driver-route/importSession"; import { transformSessionToDriverRoute } from "@/lib/driver-route/transformSession"; import type { DeliveryStop, DriverRoute } from "@/lib/driver-route/types"; @@ -24,6 +21,7 @@ import { readUploadedRouteFile, } from "./storage"; import { styles } from "./styles"; +import { ROUTE_UPLOAD_ERROR_KEY } from "@/app/upload-route/routeUploadValidation"; function openNavigation(stop: DeliveryStop) { // Prefer exact coordinates from the route file; fall back to the address if @@ -51,7 +49,6 @@ function openPhone(stop: DeliveryStop) { export default function DriverAssistPwaPage() { const router = useRouter(); - const inputRef = useRef(null); const topRef = useRef(null); const remainingRef = useRef(null); const deliveredRef = useRef(null); @@ -63,15 +60,12 @@ export default function DriverAssistPwaPage() { "Customer unavailable", ); const [reportDetails, setReportDetails] = useState(""); - const [error, setError] = useState(null); - const [isImporting, setIsImporting] = useState(false); const [hasCheckedRoute, setHasCheckedRoute] = useState(false); useEffect(() => { // The upload page hands off the raw JSON through sessionStorage so this // page can import once, save the driver shape, and avoid bouncing back. const uploadedRoute = readUploadedRouteFile(); - let importFailed = false; if (uploadedRoute) { try { @@ -79,29 +73,37 @@ export default function DriverAssistPwaPage() { const nextRoute = transformSessionToDriverRoute(session); persistRoute(nextRoute); clearUploadedRouteFile(); - setRoute(nextRoute); - setOpenId(nextRoute.stops[0]?.id || null); - setHasCheckedRoute(true); + queueMicrotask(() => { + setRoute(nextRoute); + setOpenId(nextRoute.stops[0]?.id || null); + setHasCheckedRoute(true); + }); return; } catch (importError) { - importFailed = true; - setError( + const message = importError instanceof Error ? importError.message - : "Please upload a valid JSON file.", - ); + : "Please upload a valid JSON file."; + clearUploadedRouteFile(); + sessionStorage.setItem(ROUTE_UPLOAD_ERROR_KEY, message); + router.replace("/upload-route"); + return; } } // Reloading the PWA should keep the driver exactly where they left off. const savedRoute = readSavedRoute(); - setRoute(savedRoute); - setOpenId(savedRoute?.stops[0]?.id || null); - setHasCheckedRoute(true); - if (!savedRoute && !importFailed) { + if (!savedRoute) { router.replace("/upload-route"); + return; } + + queueMicrotask(() => { + setRoute(savedRoute); + setOpenId(savedRoute.stops[0]?.id || null); + setHasCheckedRoute(true); + }); }, [router]); useEffect(() => { @@ -127,29 +129,6 @@ export default function DriverAssistPwaPage() { }; }, [route]); - const importRoute = async (file: File) => { - setError(null); - setIsImporting(true); - - try { - // Direct upload is kept here too, so /driver_assist works even if a - // driver lands on it without going through /upload-route first. - const session = await loadSessionFromFile(file); - const nextRoute = transformSessionToDriverRoute(session); - persistRoute(nextRoute); - setRoute(nextRoute); - setOpenId(nextRoute.stops[0]?.id || null); - } catch (importError) { - setError( - importError instanceof Error - ? importError.message - : "Please upload a valid JSON file.", - ); - } finally { - setIsImporting(false); - } - }; - const updateStop = (stopId: string, changes: Partial) => { // Keep stop updates narrow so notes, status, and failure reasons can share // one path without rebuilding the whole route by hand. @@ -209,41 +188,12 @@ export default function DriverAssistPwaPage() { const reportedStops = route?.stops.filter((stop) => stop.status === "failed") || []; - if (!hasCheckedRoute || (!route && !error)) { + if (!hasCheckedRoute || !route) { // Empty shell prevents the black-and-white upload screen from flashing // while local/session storage is being checked. return
; } - if (!route) { - return ( -
-
-

driver_assist

- { - const file = event.target.files?.[0]; - if (file) void importRoute(file); - }} - /> - - {error ?

{error}

: null} -
-
- ); - } - return (
diff --git a/app/ui/src/app/driver_assist/styles.ts b/app/ui/src/app/driver_assist/styles.ts index a16f4c78a..1d821f7d2 100644 --- a/app/ui/src/app/driver_assist/styles.ts +++ b/app/ui/src/app/driver_assist/styles.ts @@ -12,41 +12,12 @@ export const styles: Record = { minHeight: "100dvh", backgroundColor: "#ffffff", }, - uploadScreen: { - minHeight: "100dvh", - display: "flex", - flexDirection: "column", - justifyContent: "center", - alignItems: "center", - padding: 24, - backgroundColor: "#ffffff", - }, - hiddenInput: { - display: "none", - }, appHeader: { fontSize: 14, fontWeight: 700, color: "#202020", margin: 0, }, - uploadButton: { - backgroundColor: "#111827", - border: 0, - borderRadius: 8, - color: "#ffffff", - cursor: "pointer", - fontSize: 16, - fontWeight: 600, - padding: "14px 20px", - }, - errorText: { - color: "#b91c1c", - fontSize: 14, - marginTop: 14, - maxWidth: 300, - textAlign: "center", - }, container: { width: "100%", maxWidth: "none", diff --git a/app/ui/src/app/upload-route/page.tsx b/app/ui/src/app/upload-route/page.tsx index cb5f1f02f..055de1e12 100644 --- a/app/ui/src/app/upload-route/page.tsx +++ b/app/ui/src/app/upload-route/page.tsx @@ -3,11 +3,16 @@ export const dynamic = "force-dynamic"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import ShellNavbar from "@/app/components/ShellNavbar"; import { formatSize } from "@/app/utils/routeUtils"; +import { + parseRouteUploadText, + ROUTE_UPLOAD_ERROR_KEY, +} from "./routeUploadValidation"; + const MAX_FILE_MB = 10; const MAX_FILE_BYTES = MAX_FILE_MB * 1024 * 1024; @@ -20,6 +25,14 @@ export default function UploadRoutePage() { const dragDepth = useRef(0); const inputRef = useRef(null); + useEffect(() => { + const uploadError = sessionStorage.getItem(ROUTE_UPLOAD_ERROR_KEY); + if (!uploadError) return; + + sessionStorage.removeItem(ROUTE_UPLOAD_ERROR_KEY); + queueMicrotask(() => setError(uploadError)); + }, []); + const handleFile = (f: File) => { setError(null); if (!f.name.endsWith(".json") && !f.name.endsWith(".csv")) { @@ -60,6 +73,7 @@ export default function UploadRoutePage() { try { const text = await file.text(); + parseRouteUploadText(text); sessionStorage.setItem( "routeFile", JSON.stringify({ name: file.name, content: text }), diff --git a/app/ui/src/app/upload-route/routeUploadValidation.ts b/app/ui/src/app/upload-route/routeUploadValidation.ts new file mode 100644 index 000000000..9744b4e20 --- /dev/null +++ b/app/ui/src/app/upload-route/routeUploadValidation.ts @@ -0,0 +1,9 @@ +import { loadSessionFromText } from "@/lib/driver-route/importSession"; +import { transformSessionToDriverRoute } from "@/lib/driver-route/transformSession"; + +export const ROUTE_UPLOAD_ERROR_KEY = "routeUploadError"; + +export function parseRouteUploadText(text: string) { + const session = loadSessionFromText(text); + return transformSessionToDriverRoute(session); +} diff --git a/app/ui/src/tests/driverRouteImport.test.ts b/app/ui/src/tests/driverRouteImport.test.ts index b811e4db5..f87dba76c 100644 --- a/app/ui/src/tests/driverRouteImport.test.ts +++ b/app/ui/src/tests/driverRouteImport.test.ts @@ -1,12 +1,13 @@ import { describe, expect, it } from "vitest"; +import { parseRouteUploadText } from "@/app/upload-route/routeUploadValidation"; import { loadSessionFromText } from "@/lib/driver-route/importSession"; import { transformSessionToDriverRoute } from "@/lib/driver-route/transformSession"; import { buildSessionSave } from "@/lib/session/exportSession"; describe("driver route import", () => { it("loads a saved route-manager session into the driver_assist route shape", () => { - const session = loadSessionFromText( + const route = parseRouteUploadText( JSON.stringify( buildSessionSave( { @@ -35,7 +36,7 @@ describe("driver route import", () => { ), ); - expect(transformSessionToDriverRoute(session)).toEqual({ + expect(route).toEqual({ driverName: "driver1", routeLabel: "Route 7 - 1 stops", stops: [ @@ -105,7 +106,13 @@ describe("driver route import", () => { ); }); - it("also accepts the same session data shape without the save envelope", () => { + it("rejects invalid upload-route files before handing them to driver_assist", () => { + expect(() => parseRouteUploadText(JSON.stringify({ version: 1 }))).toThrow( + 'Invalid save file format at "savedAt".', + ); + }); + + it("also accepts a direct optimize request JSON file", () => { const session = loadSessionFromText( JSON.stringify({ deliveries: [