From 2c4bd1c00ca4627f203b109d707d2d46e9057cdd Mon Sep 17 00:00:00 2001 From: OziinG <145884442+OziinG@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:09:44 +0900 Subject: [PATCH 1/2] Show cached OSRM route metrics in admin routes Constraint: Routes overview/detail must use routeMetrics only and render blanks for missing cache.\nRejected: Falling back to legacy ETA/distance fields | hides stale or guessed values.\nConfidence: high\nScope-risk: moderate\nDirective: Keep parent summary totals leaf-row based when parent rows display child aggregates.\nTested: cd apps/shopify-app && node --test tests/routes-page.test.mjs; npm run typecheck; npm run build; node --test tests/*.test.mjs; npm run check:public-urls.\nNot-tested: Browser iframe smoke after backend deploy. --- .../features/delivery/route-detail.server.js | 2 +- .../app/routes/app.routes.$routeId.jsx | 4 +- apps/shopify-app/app/routes/app.routes.jsx | 154 +++++++++--------- apps/shopify-app/tests/routes-page.test.mjs | 44 ++++- 4 files changed, 118 insertions(+), 86 deletions(-) diff --git a/apps/shopify-app/app/features/delivery/route-detail.server.js b/apps/shopify-app/app/features/delivery/route-detail.server.js index d2ea631..278a78c 100644 --- a/apps/shopify-app/app/features/delivery/route-detail.server.js +++ b/apps/shopify-app/app/features/delivery/route-detail.server.js @@ -79,7 +79,7 @@ export function buildRouteGroupChildDetails(routeGroup) { const optimized = readRouteOptimizedSnapshot(child?.optimized ?? routePlan?.optimized); return { routeGeometry: child?.routeGeometry ?? routePlan?.routeGeometry ?? optimized?.routeGeometry ?? null, - routeMetrics: child?.routeMetrics ?? routePlan?.routeMetrics ?? routePlan?.metrics ?? optimized?.metrics ?? null, + routeMetrics: child?.routeMetrics ?? routePlan?.routeMetrics ?? null, routePlan: getRouteGroupChildRoutePlan(routeGroup, child, routePlanId, index, stops), routePlanId, routeStopPoints: firstArray(child?.routeStopPoints, routePlan?.routeStopPoints, optimized?.routeStopPoints), diff --git a/apps/shopify-app/app/routes/app.routes.$routeId.jsx b/apps/shopify-app/app/routes/app.routes.$routeId.jsx index 34884c9..ef57194 100644 --- a/apps/shopify-app/app/routes/app.routes.$routeId.jsx +++ b/apps/shopify-app/app/routes/app.routes.$routeId.jsx @@ -1387,8 +1387,8 @@ export default function RouteDetailPage() { const routeDeliveredCount = countRouteStopsByStatus(orderedRouteStops, ["DELIVERED", "FULFILLED"]); const routeAttemptedCount = countRouteStopsByStatus(orderedRouteStops, ["ATTEMPTED", "FAILED"]); const routeTotalItems = getRouteTotalItems(effectiveRoutePlan, orderedRouteStops); - const routeTotalDriveTime = getRouteMetricLabel(formatRouteDurationSeconds(routeMetrics?.durationSeconds), effectiveRoutePlan?.totalDriveTime, effectiveRoutePlan?.driveTime); - const routeTotalDistance = getRouteMetricLabel(formatRouteDistanceMeters(routeMetrics?.distanceMeters), effectiveRoutePlan?.totalDistance, effectiveRoutePlan?.distance); + const routeTotalDriveTime = getRouteMetricLabel(formatRouteDurationSeconds(routeMetrics?.durationSeconds)); + const routeTotalDistance = getRouteMetricLabel(formatRouteDistanceMeters(routeMetrics?.distanceMeters)); const routeTotalWeight = getRouteMetricLabel(effectiveRoutePlan?.totalWeight, effectiveRoutePlan?.weight); const routeVehicleLabel = getRouteVehicleLabel(effectiveRoutePlan); const routeCreatedLabel = getRouteCreatedLabel(effectiveRoutePlan); diff --git a/apps/shopify-app/app/routes/app.routes.jsx b/apps/shopify-app/app/routes/app.routes.jsx index 685038e..26bc00c 100644 --- a/apps/shopify-app/app/routes/app.routes.jsx +++ b/apps/shopify-app/app/routes/app.routes.jsx @@ -394,11 +394,19 @@ function sumOptionalNumbers(values) { return hasValue ? total : null; } -function formatDriveTime(totalMinutes) { - const minutes = numberOrNull(totalMinutes); - if (minutes == null) return "-"; +function readRouteMetrics(routePlan) { + const routeMetrics = routePlan?.routeMetrics ?? null; + return { + distanceMeters: firstNumber(routeMetrics?.distanceMeters), + durationSeconds: firstNumber(routeMetrics?.durationSeconds), + }; +} + +function formatRouteDurationSeconds(totalSeconds) { + const seconds = numberOrNull(totalSeconds); + if (seconds == null) return "-"; - const roundedMinutes = Math.max(Math.round(minutes), 0); + const roundedMinutes = Math.max(Math.round(seconds / 60), 0); const hours = Math.floor(roundedMinutes / 60); const remainingMinutes = roundedMinutes % 60; @@ -407,12 +415,13 @@ function formatDriveTime(totalMinutes) { return `${hours} hr ${remainingMinutes} min`; } -function formatDistanceMiles(totalDistanceMiles) { - const distanceMiles = numberOrNull(totalDistanceMiles); - if (distanceMiles == null) return "-"; +function formatRouteDistanceMeters(totalDistanceMeters) { + const distanceMeters = numberOrNull(totalDistanceMeters); + if (distanceMeters == null) return "-"; - const roundedDistanceMiles = Math.round(distanceMiles * 10) / 10; - return `${Number.isInteger(roundedDistanceMiles) ? roundedDistanceMiles : roundedDistanceMiles.toFixed(1)}mi`; + const kilometers = distanceMeters / 1000; + const roundedKilometers = Math.round(kilometers * 10) / 10; + return `${Number.isInteger(roundedKilometers) ? roundedKilometers : roundedKilometers.toFixed(1)} km`; } function formatRouteTableDate(routePlan) { @@ -441,37 +450,12 @@ function buildRouteRows(routePlans, routeGroups = []) { const standaloneRoutePlans = Array.isArray(routePlans) ? routePlans.filter((routePlan) => !childRoutePlanIds.has(routePlan.id)) : []; - const routeGroupRows = safeRouteGroups.map((routeGroup) => { - const childCount = getVisibleRouteGroupChildren(routeGroup).length; - return { - id: routeGroup.id, - rowKey: `routeGroup:${routeGroup.id}`, - routeGroupId: routeGroup.id, - href: routeGroupPath(routeGroup.id), - isClickable: true, - isDeletable: true, - isRouteGroup: true, - deleteKey: getRouteDeleteKey({ ...routeGroup, isRouteGroup: true }), - route: routeGroup.name ?? routeGroup.id, - status: routeGroup.displayStatus ?? routeGroup.status ?? "DRAFT", - orders: getRouteGroupTotalOrders(routeGroup), - coordinates: "-", - delivered: 0, - attempted: 0, - missingCoordinates: 0, - date: formatRouteGroupDate(routeGroup), - deliveryArea: "-", - start: "Parent group", - end: childCount > 0 ? `${childCount} child routes` : "No split", - driver: "-", - driverId: null, - }; - }); const routeChildRows = safeRouteGroups.flatMap((routeGroup) => getVisibleRouteGroupChildren(routeGroup) .map((child, index) => { const routePlanId = getRouteGroupChildRoutePlanId(child); const routePlan = child.routePlan ?? {}; + const routeMetrics = readRouteMetrics({ ...routePlan, routeMetrics: child.routeMetrics ?? routePlan.routeMetrics }); const stopsCount = child.stopsCount ?? routePlan.stopsCount ?? 0; const missingCoordinates = routePlan.missingCoordinates ?? 0; const locatedCount = Math.max(stopsCount - missingCoordinates, 0); @@ -493,27 +477,51 @@ function buildRouteRows(routePlans, routeGroups = []) { missingCoordinates, date: formatRouteTableDate(routePlan), deliveryArea: formatRouteValues(routePlan.deliveryAreas), - start: "Shopify departure location", - end: "Loop back to start", driver: formatRouteDriver({ displayName: child.driverName }), driverId: child.driverId ?? routePlan.driverId ?? null, - driveTimeMinutes: firstNumber( - routePlan.driveTimeMinutes, - routePlan.totalDriveTimeMinutes, - routePlan.durationMinutes, - routePlan.metrics?.driveTimeMinutes, - routePlan.metrics?.totalDriveTimeMinutes, - ), - distanceMiles: firstNumber( - routePlan.distanceMiles, - routePlan.totalDistanceMiles, - routePlan.distanceMi, - routePlan.metrics?.distanceMiles, - routePlan.metrics?.totalDistanceMiles, - ), + driveTimeSeconds: routeMetrics.durationSeconds, + distanceMeters: routeMetrics.distanceMeters, }; }), ); + const routeGroupMetricsById = new Map( + safeRouteGroups.map((routeGroup) => { + const childRows = routeChildRows.filter((routeRow) => routeRow.parentRouteGroupId === routeGroup.id); + return [ + routeGroup.id, + { + distanceMeters: sumOptionalNumbers(childRows.map((routeRow) => routeRow.distanceMeters)), + durationSeconds: sumOptionalNumbers(childRows.map((routeRow) => routeRow.driveTimeSeconds)), + }, + ]; + }), + ); + const routeGroupRows = safeRouteGroups.map((routeGroup) => { + const routeMetrics = routeGroupMetricsById.get(routeGroup.id) ?? {}; + return { + id: routeGroup.id, + rowKey: `routeGroup:${routeGroup.id}`, + routeGroupId: routeGroup.id, + href: routeGroupPath(routeGroup.id), + isClickable: true, + isDeletable: true, + isRouteGroup: true, + deleteKey: getRouteDeleteKey({ ...routeGroup, isRouteGroup: true }), + route: routeGroup.name ?? routeGroup.id, + status: routeGroup.displayStatus ?? routeGroup.status ?? "DRAFT", + orders: getRouteGroupTotalOrders(routeGroup), + coordinates: "-", + delivered: 0, + attempted: 0, + missingCoordinates: 0, + date: formatRouteGroupDate(routeGroup), + deliveryArea: "-", + driver: "-", + driverId: null, + driveTimeSeconds: routeMetrics.durationSeconds ?? null, + distanceMeters: routeMetrics.distanceMeters ?? null, + }; + }); if (standaloneRoutePlans.length === 0 && routeGroupRows.length === 0 && routeChildRows.length === 0) { return [ @@ -526,15 +534,16 @@ function buildRouteRows(routePlans, routeGroups = []) { orders: 0, date: "-", deliveryArea: "-", - start: "Shopify departure location", - end: "Loop back to start", driver: "-", driverId: null, + driveTimeSeconds: null, + distanceMeters: null, }, ]; } const routePlanRows = standaloneRoutePlans.map((routePlan) => { + const routeMetrics = readRouteMetrics(routePlan); const stopsCount = routePlan.stopsCount ?? 0; const missingCoordinates = routePlan.missingCoordinates ?? 0; const locatedCount = Math.max(stopsCount - missingCoordinates, 0); @@ -567,24 +576,10 @@ function buildRouteRows(routePlans, routeGroups = []) { missingCoordinates, date: formatRouteTableDate(routePlan), deliveryArea: formatRouteValues(routePlan.deliveryAreas), - start: "Shopify departure location", - end: "Loop back to start", driver: formatRouteDriver(routePlan.driver), driverId: routePlan.driverId ?? routePlan.driver?.id ?? null, - driveTimeMinutes: firstNumber( - routePlan.driveTimeMinutes, - routePlan.totalDriveTimeMinutes, - routePlan.durationMinutes, - routePlan.metrics?.driveTimeMinutes, - routePlan.metrics?.totalDriveTimeMinutes, - ), - distanceMiles: firstNumber( - routePlan.distanceMiles, - routePlan.totalDistanceMiles, - routePlan.distanceMi, - routePlan.metrics?.distanceMiles, - routePlan.metrics?.totalDistanceMiles, - ), + driveTimeSeconds: routeMetrics.durationSeconds, + distanceMeters: routeMetrics.distanceMeters, }; }); return [...routeGroupRows, ...routeChildRows, ...routePlanRows]; @@ -599,19 +594,20 @@ function formatRouteGroupDate(routeGroup) { function buildRoutesSummary(routeRows) { const activeRouteRows = routeRows.filter((route) => route.isClickable); + const summaryRouteRows = activeRouteRows.filter((route) => !route.isRouteGroup); return [ - { label: "Routes", value: String(activeRouteRows.length) }, - { label: "Stops", value: String(sumNumbers(activeRouteRows.map((route) => route.orders))) }, - { label: "Delivered", value: String(sumNumbers(activeRouteRows.map((route) => route.delivered))) }, - { label: "Attempted", value: String(sumNumbers(activeRouteRows.map((route) => route.attempted))) }, + { label: "Routes", value: String(summaryRouteRows.length) }, + { label: "Stops", value: String(sumNumbers(summaryRouteRows.map((route) => route.orders))) }, + { label: "Delivered", value: String(sumNumbers(summaryRouteRows.map((route) => route.delivered))) }, + { label: "Attempted", value: String(sumNumbers(summaryRouteRows.map((route) => route.attempted))) }, { label: "Drive time", - value: formatDriveTime(sumOptionalNumbers(activeRouteRows.map((route) => route.driveTimeMinutes))), + value: formatRouteDurationSeconds(sumOptionalNumbers(summaryRouteRows.map((route) => route.driveTimeSeconds))), }, { label: "Distance", - value: formatDistanceMiles(sumOptionalNumbers(activeRouteRows.map((route) => route.distanceMiles))), + value: formatRouteDistanceMeters(sumOptionalNumbers(summaryRouteRows.map((route) => route.distanceMeters))), }, ]; } @@ -841,8 +837,8 @@ export default function RoutesPage() { Orders Area Driver - Start - End + Total drive time + Total distance @@ -875,8 +871,8 @@ export default function RoutesPage() { {route.orders} {route.deliveryArea} {route.driver} - {route.start} - {route.end} + {formatRouteDurationSeconds(route.driveTimeSeconds)} + {formatRouteDistanceMeters(route.distanceMeters)} ))} diff --git a/apps/shopify-app/tests/routes-page.test.mjs b/apps/shopify-app/tests/routes-page.test.mjs index 5f9a912..e78a468 100644 --- a/apps/shopify-app/tests/routes-page.test.mjs +++ b/apps/shopify-app/tests/routes-page.test.mjs @@ -160,8 +160,8 @@ test("Routes table uses aligned CLEVER planning columns", () => { assert.match(routesPageSource, />Status<\/th>/); assert.match(routesPageSource, />Orders<\/th>/); assert.match(routesPageSource, />Area<\/th>/); - assert.match(routesPageSource, />Start<\/th>/); - assert.match(routesPageSource, />End<\/th>/); + assert.match(routesPageSource, />Total drive time<\/th>/); + assert.match(routesPageSource, />Total distance<\/th>/); assert.match(routesPageSource, />Driver<\/th>/); assert.doesNotMatch(routesPageSource, />Planned for<\/th>/); assert.doesNotMatch(routesPageSource, />Delivery date<\/th>/); @@ -175,8 +175,9 @@ test("Routes table uses aligned CLEVER planning columns", () => { assert.match(routesPageSource, /return Number\(routeGroup\?\.totalOrders \?\? routeGroup\?\.ordersCount \?\? routeGroup\?\.assignments\?\.length \?\? 0\) \|\| 0/); assert.doesNotMatch(routeHelpersSource, /return children\.length >= 2 \? children : \[\]/); assert.match(routeHelpersSource, /rightRouteIdx = numberOrUndefined\(right\.child\?\.routeIdx\)/); - assert.match(routesPageSource, /const childCount = getVisibleRouteGroupChildren\(routeGroup\)\.length/); - assert.match(routesPageSource, /end: childCount > 0 \? `\$\{childCount\} child routes` : "No split"/); + assert.match(routesPageSource, /const routeGroupMetricsById = new Map/); + assert.match(routesPageSource, /distanceMeters: sumOptionalNumbers\(childRows\.map\(\(routeRow\) => routeRow\.distanceMeters\)\)/); + assert.match(routesPageSource, /durationSeconds: sumOptionalNumbers\(childRows\.map\(\(routeRow\) => routeRow\.driveTimeSeconds\)\)/); assert.match(routesPageSource, /isRouteGroup: true/); assert.match(routesPageSource, /isDeletable: false/); assert.match(routesPageSource, /formatRouteGroupDate\(routeGroup\)/); @@ -190,6 +191,29 @@ test("Routes table uses aligned CLEVER planning columns", () => { assert.doesNotMatch(routesPageSource, />Delivery day<\/th>/); }); +test("Routes table renders OSRM drive metrics instead of start and end placeholders", () => { + assert.match(routesPageSource, />Total drive time<\/th>/); + assert.match(routesPageSource, />Total distance<\/th>/); + assert.doesNotMatch(routesPageSource, />Start<\/th>/); + assert.doesNotMatch(routesPageSource, />End<\/th>/); + assert.match(routesPageSource, /function readRouteMetrics\(routePlan\) \{/); + assert.match(routesPageSource, /routeMetrics\?\.durationSeconds/); + assert.match(routesPageSource, /routeMetrics\?\.distanceMeters/); + assert.match(routesPageSource, /formatRouteDurationSeconds\(route\.driveTimeSeconds\)/); + assert.match(routesPageSource, /formatRouteDistanceMeters\(route\.distanceMeters\)/); + assert.match(routesPageSource, /const routeGroupMetricsById = new Map/); + assert.match(routesPageSource, /parentRouteGroupId: routeGroup\.id/); + assert.match(routesPageSource, /const summaryRouteRows = activeRouteRows\.filter\(\(route\) => !route\.isRouteGroup\)/); + assert.match(routesPageSource, /value: String\(summaryRouteRows\.length\)/); + assert.match(routesPageSource, /sumNumbers\(summaryRouteRows\.map\(\(route\) => route\.orders\)\)/); + assert.match(routesPageSource, /sumNumbers\(summaryRouteRows\.map\(\(route\) => route\.delivered\)\)/); + assert.match(routesPageSource, /sumNumbers\(summaryRouteRows\.map\(\(route\) => route\.attempted\)\)/); + assert.match(routesPageSource, /sumOptionalNumbers\(summaryRouteRows\.map\(\(route\) => route\.driveTimeSeconds\)\)/); + assert.match(routesPageSource, /sumOptionalNumbers\(summaryRouteRows\.map\(\(route\) => route\.distanceMeters\)\)/); + assert.doesNotMatch(routesPageSource, /driveTimeMinutes: firstNumber/); + assert.doesNotMatch(routesPageSource, /distanceMiles: firstNumber/); +}); + test("Routes page removes the previous copied detail and settings rules", () => { assert.doesNotMatch(routesPageSource, /selectedRouteId/); assert.doesNotMatch(routesPageSource, /const routeMetricsGridStyle = \{/); @@ -278,6 +302,18 @@ test("Route detail loader reads server-saved drivers for route driver labels", ( assert.match(routeDetailServerSource, /driverData\.errors/); }); +test("Route detail drive metrics are OSRM-only and use child DTO metrics", () => { + assert.match(routeDetailServerSource, /routeMetrics: child\?\.routeMetrics \?\? routePlan\?\.routeMetrics \?\? null/); + assert.match(routeDetailSource, /driveTimeLabel: getRouteMetricLabel\(formatRouteDurationSeconds\(detail\?\.routeMetrics\?\.durationSeconds\)\)/); + assert.match(routeDetailSource, /totalDistanceLabel: getRouteMetricLabel\(formatRouteDistanceMeters\(detail\?\.routeMetrics\?\.distanceMeters\)\)/); + assert.match(routeDetailSource, /const routeTotalDriveTime = getRouteMetricLabel\(formatRouteDurationSeconds\(routeMetrics\?\.durationSeconds\)\)/); + assert.match(routeDetailSource, /const routeTotalDistance = getRouteMetricLabel\(formatRouteDistanceMeters\(routeMetrics\?\.distanceMeters\)\)/); + assert.doesNotMatch(routeDetailSource, /effectiveRoutePlan\?\.totalDriveTime/); + assert.doesNotMatch(routeDetailSource, /effectiveRoutePlan\?\.driveTime/); + assert.doesNotMatch(routeDetailSource, /effectiveRoutePlan\?\.totalDistance/); + assert.doesNotMatch(routeDetailSource, /effectiveRoutePlan\?\.distance/); +}); + test("Route detail keeps the server driver action but removes the header assignment UI", () => { assert.match(routeDetailServerSource, /assignDeliveryRoutePlanDriver/); assert.match(routeDetailServerSource, /intent === "saveRouteDriver"/); From 7c2b5c00deb6f0654018544683c59853f74a0208 Mon Sep 17 00:00:00 2001 From: OziinG <145884442+OziinG@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:48:35 +0900 Subject: [PATCH 2/2] Render route group rows from child metrics Constraint: Route group detail rows must show child-route OSRM metrics and item totals after server draft-save cache generation.\nRejected: Reusing parent route summary values | group rows can hide missing child metrics and stale totals.\nConfidence: high\nScope-risk: narrow\nDirective: Keep routeIdx/#idx as server-owned identity and use childRoutePlan/childRouteMetrics for row totals.\nTested: node --test tests/routes-page.test.mjs tests/route-groups-helper.test.mjs; npm run typecheck; git diff --check\nNot-tested: Shopify app production deploy. --- .../app/routes/app.routes.$routeId.jsx | 35 ++++++++++++------- apps/shopify-app/tests/routes-page.test.mjs | 14 +++++--- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/apps/shopify-app/app/routes/app.routes.$routeId.jsx b/apps/shopify-app/app/routes/app.routes.$routeId.jsx index ef57194..425531a 100644 --- a/apps/shopify-app/app/routes/app.routes.$routeId.jsx +++ b/apps/shopify-app/app/routes/app.routes.$routeId.jsx @@ -783,6 +783,8 @@ function getRouteStartDateTimeValue(routePlan) { if (value?.includes("T")) return value.slice(0, 16); const date = textOrUndefined(routePlan?.routeScope?.deliveryDate ?? routePlan?.deliveryDate ?? routePlan?.planDate); + const departureTime = textOrUndefined(routePlan?.departureTime); + if (date && departureTime) return `${date}T${departureTime}`; return date ? `${date}T09:00` : ""; } @@ -807,7 +809,7 @@ function countRouteStopsByStatus(routeStops, statuses) { } function getRouteTotalItems(routePlan, routeStops) { - const explicitTotal = numberOrUndefined(routePlan?.totalItems ?? routePlan?.itemsCount ?? routePlan?.itemCount); + const explicitTotal = numberOrUndefined(routePlan?.itemSummary?.totalQuantity ?? routePlan?.totalItems ?? routePlan?.itemsCount ?? routePlan?.itemCount); const stopTotal = routeStops.reduce((total, stop) => total + (numberOrUndefined(stop.itemCount) ?? 0), 0); return explicitTotal ?? (stopTotal > 0 ? stopTotal : ROUTE_EMPTY_LABEL); @@ -918,6 +920,8 @@ function buildRouteStops(stops) { const stopNumber = Number.isInteger(sequence) && sequence > 0 ? sequence : index + 1; + const itemCount = numberOrUndefined(stop.itemCount ?? stop.itemsCount ?? stop.totalItems) + ?? firstArray(stop.items).reduce((total, item) => total + (numberOrUndefined(item?.quantity) ?? 0), 0); return { id: stop.deliveryStopId ?? stop.shopifyOrderGid ?? `route-stop-${index + 1}`, @@ -934,7 +938,7 @@ function buildRouteStops(stops) { status: stop.fulfillmentStatus ?? stop.status ?? stop.assignmentStatus ?? "PENDING", payment: stop.paymentStatus ?? stop.financialStatus ?? "—", attributes: formatStopAttributes(stop.attributes), - itemCount: numberOrUndefined(stop.itemCount ?? stop.itemsCount ?? stop.totalItems), + itemCount, coordinatesLabel: coordinates != null ? "Yes" : "No", coordinates, hasCoordinates: coordinates != null, @@ -1065,13 +1069,15 @@ function buildRouteGroupChildRows(routeGroup, childDetailsByRoutePlanId = new Ma const routePlanId = getRouteGroupChildRoutePlanId(child); const detail = childDetailsByRoutePlanId.get(routePlanId); const detailStops = detail?.stops ?? []; + const childRoutePlan = detail?.routePlan ?? child?.routePlan ?? null; + const childRouteMetrics = detail?.routeMetrics ?? child?.routeMetrics ?? childRoutePlan?.routeMetrics ?? null; const orderIds = getRouteGroupChildOrderIds(child, detailStops, routeStops); const stopByOrderId = new Map(routeStops.map((stop) => [stop.orderId, stop])); const stops = orderIds.length > 0 ? orderIds.map((orderId) => detailStops.find((stop) => stop.orderId === orderId) ?? stopByOrderId.get(orderId)).filter(Boolean) : detailStops; const optimized = { - metrics: detail?.routeMetrics ?? null, + metrics: childRouteMetrics, routeGeometry: detail?.routeGeometry ?? null, routeStopPoints: detail?.routeStopPoints ?? [], }; @@ -1079,10 +1085,10 @@ function buildRouteGroupChildRows(routeGroup, childDetailsByRoutePlanId = new Ma return { attemptedCount: countRouteStopsByStatus(stops, ["ATTEMPTED", "FAILED", "NEEDS_REVIEW"]), color: textOrUndefined(child?.color) ?? ROUTE_DEFAULT_COLORS[index % ROUTE_DEFAULT_COLORS.length] ?? MAP_MARKER_PALETTE.plannedOrder.color, - createdLabel: textOrUndefined(detail?.routePlan?.createdAt)?.replace("T", " ").slice(0, 16) ?? ROUTE_EMPTY_LABEL, + createdLabel: getRouteCreatedLabel(childRoutePlan), deliveredCount: countRouteStopsByStatus(stops, ["DELIVERED", "FULFILLED"]), - driverLabel: textOrUndefined(child?.driverName ?? detail?.routePlan?.driver?.displayName) ?? "Unassigned", - driveTimeLabel: getRouteMetricLabel(formatRouteDurationSeconds(detail?.routeMetrics?.durationSeconds)), + driverLabel: textOrUndefined(child?.driverName ?? childRoutePlan?.driver?.displayName) ?? "Unassigned", + driveTimeLabel: getRouteMetricLabel(formatRouteDurationSeconds(childRouteMetrics?.durationSeconds)), id: routePlanId ?? `group-route-${index}`, isCurrent: false, optimized, @@ -1091,14 +1097,15 @@ function buildRouteGroupChildRows(routeGroup, childDetailsByRoutePlanId = new Ma routeKey: routePlanId ? `routePlan:${routePlanId}` : `routeIdx:${routeIndex}`, routeIndex, routePlanId: routePlanId ?? null, - status: formatRouteStatus(detail?.routePlan?.status ?? child?.displayStatus ?? child?.status), + startTimeLabel: getRouteStartTimeLabel(getRouteStartDateTimeValue(childRoutePlan)), + status: formatRouteStatus(childRoutePlan?.status ?? child?.displayStatus ?? child?.status), stops, stopsCount: stops.length || orderIds.length, - title: getRouteGroupChildRouteName(routeGroup, child, detail?.routePlan ?? child?.routePlan, index), - totalDistanceLabel: getRouteMetricLabel(formatRouteDistanceMeters(detail?.routeMetrics?.distanceMeters)), - totalItems: getRouteTotalItems(detail?.routePlan, stops), - totalWeightLabel: getRouteMetricLabel(detail?.routePlan?.totalWeight, detail?.routePlan?.weight), - vehicleLabel: getRouteVehicleLabel(detail?.routePlan), + title: getRouteGroupChildRouteName(routeGroup, child, childRoutePlan, index), + totalDistanceLabel: getRouteMetricLabel(formatRouteDistanceMeters(childRouteMetrics?.distanceMeters)), + totalItems: getRouteTotalItems(childRoutePlan, stops), + totalWeightLabel: getRouteMetricLabel(childRoutePlan?.totalWeight, childRoutePlan?.weight), + vehicleLabel: getRouteVehicleLabel(childRoutePlan), }; }).filter((routeRow) => routeRow.routePlanId); routeGroupChildRows.sort((first, second) => ( @@ -1459,6 +1466,7 @@ export default function RouteDetailPage() { routeIndex: numberOrUndefined(currentRouteGroupChild?.routeIdx) ?? 1, routeKey: `routePlan:${textOrUndefined(effectiveRoutePlan?.id) ?? currentRouteLineId}`, routePlanId: textOrUndefined(effectiveRoutePlan?.id) ?? null, + startTimeLabel: routeStartTimeLabel, status: formatRouteStatus(effectiveRoutePlan?.status), stops: orderedRouteStops, stopsCount: orderedRouteStops.length, @@ -1868,6 +1876,7 @@ export default function RouteDetailPage() { routeIdx: draft.routeIdx, routeIndex: draft.routeIndex, routePlanId: null, + startTimeLabel: ROUTE_EMPTY_LABEL, stops: [], stopsCount: 0, tempId, @@ -2585,7 +2594,7 @@ export default function RouteDetailPage() { diff --git a/apps/shopify-app/tests/routes-page.test.mjs b/apps/shopify-app/tests/routes-page.test.mjs index e78a468..7b470c4 100644 --- a/apps/shopify-app/tests/routes-page.test.mjs +++ b/apps/shopify-app/tests/routes-page.test.mjs @@ -304,8 +304,9 @@ test("Route detail loader reads server-saved drivers for route driver labels", ( test("Route detail drive metrics are OSRM-only and use child DTO metrics", () => { assert.match(routeDetailServerSource, /routeMetrics: child\?\.routeMetrics \?\? routePlan\?\.routeMetrics \?\? null/); - assert.match(routeDetailSource, /driveTimeLabel: getRouteMetricLabel\(formatRouteDurationSeconds\(detail\?\.routeMetrics\?\.durationSeconds\)\)/); - assert.match(routeDetailSource, /totalDistanceLabel: getRouteMetricLabel\(formatRouteDistanceMeters\(detail\?\.routeMetrics\?\.distanceMeters\)\)/); + assert.match(routeDetailSource, /const childRouteMetrics = detail\?\.routeMetrics \?\? child\?\.routeMetrics \?\? childRoutePlan\?\.routeMetrics \?\? null/); + assert.match(routeDetailSource, /driveTimeLabel: getRouteMetricLabel\(formatRouteDurationSeconds\(childRouteMetrics\?\.durationSeconds\)\)/); + assert.match(routeDetailSource, /totalDistanceLabel: getRouteMetricLabel\(formatRouteDistanceMeters\(childRouteMetrics\?\.distanceMeters\)\)/); assert.match(routeDetailSource, /const routeTotalDriveTime = getRouteMetricLabel\(formatRouteDurationSeconds\(routeMetrics\?\.durationSeconds\)\)/); assert.match(routeDetailSource, /const routeTotalDistance = getRouteMetricLabel\(formatRouteDistanceMeters\(routeMetrics\?\.distanceMeters\)\)/); assert.doesNotMatch(routeDetailSource, /effectiveRoutePlan\?\.totalDriveTime/); @@ -456,7 +457,7 @@ test("Route detail separates group and child titles", () => { assert.match(routeHelpersSource, /function getRouteGroupChildRouteName\(routeGroup, child, routePlan, index\) \{/); assert.match(routeHelpersSource, /name\.startsWith\(`\$\{groupName\} — `\)/); assert.match(routeDetailServerSource, /name: getRouteGroupChildRouteName\(routeGroup, child, routePlan, index\)/); - assert.match(routeDetailSource, /title: getRouteGroupChildRouteName\(routeGroup, child, detail\?\.routePlan \?\? child\?\.routePlan, index\)/); + assert.match(routeDetailSource, /title: getRouteGroupChildRouteName\(routeGroup, child, childRoutePlan, index\)/); assert.match(routeDetailServerSource, /routePlan: currentChildDetail\?\.routePlan \?\? routePlanData\.routePlan/); assert.match(routeDetailSource, /

\{routeDetailTitle\}<\/h1>/); }); @@ -862,7 +863,12 @@ test("Route detail renders route lines and a stop timeline below the map", () => assert.match(routeDetailSource, />Total weight<\/th>/); assert.match(routeDetailSource, />Created<\/th>/); assert.match(routeDetailSource, /const defaultRouteCandidateTitle = isRouteGroupDetail \? "#1" : routeDetailTitle/); - assert.match(routeDetailSource, /title: getRouteGroupChildRouteName\(routeGroup, child, detail\?\.routePlan \?\? child\?\.routePlan, index\)/); + assert.match(routeDetailSource, /const childRoutePlan = detail\?\.routePlan \?\? child\?\.routePlan \?\? null/); + assert.match(routeDetailSource, /title: getRouteGroupChildRouteName\(routeGroup, child, childRoutePlan, index\)/); + assert.match(routeDetailSource, /startTimeLabel: getRouteStartTimeLabel\(getRouteStartDateTimeValue\(childRoutePlan\)\)/); + assert.match(routeDetailSource, /totalItems: getRouteTotalItems\(childRoutePlan, stops\)/); + assert.match(routeDetailSource, /routePlan\?\.itemSummary\?\.totalQuantity \?\? routePlan\?\.totalItems/); + assert.match(routeDetailSource, /routeRow\.startTimeLabel \?\? routeStartTimeLabel/); assert.match(routeDetailSource, /aria-label="Change route driver"/); assert.match(routeDetailSource, /aria-label="Change route vehicle"/); assert.match(routeDetailSource, /aria-label="Change route start time"/);