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 48fab12..d2ea631 100644 --- a/apps/shopify-app/app/features/delivery/route-detail.server.js +++ b/apps/shopify-app/app/features/delivery/route-detail.server.js @@ -316,11 +316,11 @@ function readRouteDraftPayload(value) { if (!Array.isArray(parsed?.routes)) return { routes: [] }; return { routes: parsed.routes.map((route) => ({ - branchId: textOrUndefined(route?.branchId) ?? null, color: textOrUndefined(route?.color) ?? null, label: textOrUndefined(route?.label) ?? null, ...(route?.optimized === undefined ? {} : { optimized: route?.optimized && typeof route.optimized === "object" ? route.optimized : null }), orderIds: Array.isArray(route?.orderIds) ? route.orderIds.map(textOrUndefined).filter(Boolean) : [], + routeIdx: Number.isFinite(Number(route?.routeIdx)) ? Number(route.routeIdx) : undefined, routeKey: textOrUndefined(route?.routeKey), routePlanId: textOrUndefined(route?.routePlanId) ?? null, sortOrder: Number.isFinite(Number(route?.sortOrder)) ? Number(route.sortOrder) : undefined, diff --git a/apps/shopify-app/app/features/delivery/route-helpers.js b/apps/shopify-app/app/features/delivery/route-helpers.js index a943f35..2c657e5 100644 --- a/apps/shopify-app/app/features/delivery/route-helpers.js +++ b/apps/shopify-app/app/features/delivery/route-helpers.js @@ -21,8 +21,9 @@ export function readRouteOptimizedSnapshot(value) { } export function getDefaultRouteGroupChildName(index, child) { + const routeIdx = numberOrUndefined(child?.routeIdx); const sortOrder = numberOrUndefined(child?.sortOrder); - return `Route ${sortOrder ?? index + 1}`; + return `Route ${routeIdx ?? sortOrder ?? index + 1}`; } export function getRouteGroupChildRouteName(routeGroup, child, routePlan, index) { @@ -43,7 +44,14 @@ export function getRouteGroupChildren(routeGroup) { export function getVisibleRouteGroupChildren(routeGroup) { const children = getRouteGroupChildren(routeGroup); - return children.length >= 2 ? children : []; + return children + .map((child, index) => ({ child, index })) + .sort((left, right) => { + const leftRouteIdx = numberOrUndefined(left.child?.routeIdx) ?? numberOrUndefined(left.child?.sortOrder) ?? left.index + 1; + const rightRouteIdx = numberOrUndefined(right.child?.routeIdx) ?? numberOrUndefined(right.child?.sortOrder) ?? right.index + 1; + return leftRouteIdx - rightRouteIdx || left.index - right.index; + }) + .map(({ child }) => child); } export function formatRouteDeliveryScope(routePlan, emptyLabel = "-") { diff --git a/apps/shopify-app/app/routes/app.routes.$routeId.jsx b/apps/shopify-app/app/routes/app.routes.$routeId.jsx index e6fa7c8..3e1179b 100644 --- a/apps/shopify-app/app/routes/app.routes.$routeId.jsx +++ b/apps/shopify-app/app/routes/app.routes.$routeId.jsx @@ -1018,51 +1018,6 @@ function formatStopAttributes(attributes) { .join(", ") || "—"; } -function buildRouteBranchRows(routeGroup, routeStops = [], childDetailsByRoutePlanId = new Map()) { - const branches = [...(routeGroup?.branches ?? [])].sort((first, second) => { - return (numberOrUndefined(first.sortOrder) ?? 0) - (numberOrUndefined(second.sortOrder) ?? 0); - }); - const stopByOrderId = new Map(routeStops.map((stop) => [stop.orderId, stop])); - - return branches.map((branch, index) => { - const routeIndex = Math.max(numberOrUndefined(branch.sortOrder) ?? index + 2, index + 2); - const orderIds = Array.isArray(branch.orderIds) ? branch.orderIds.map(textOrUndefined).filter(Boolean) : []; - const branchStops = orderIds.map((orderId) => stopByOrderId.get(orderId)).filter(Boolean); - const childDetail = childDetailsByRoutePlanId.get(textOrUndefined(branch.routePlanId)); - const optimized = readRouteOptimizedSnapshot(branch.optimized) ?? (childDetail - ? { - metrics: childDetail.routeMetrics ?? null, - routeGeometry: childDetail.routeGeometry ?? null, - routeStopPoints: childDetail.routeStopPoints ?? [], - } - : null); - return { - attemptedCount: 0, - color: textOrUndefined(branch.color) ?? ROUTE_DEFAULT_COLORS[(index + 1) % ROUTE_DEFAULT_COLORS.length] ?? MAP_MARKER_PALETTE.plannedOrder.color, - createdLabel: textOrUndefined(branch.createdAt)?.replace("T", " ").slice(0, 16) ?? ROUTE_EMPTY_LABEL, - deliveredCount: 0, - driverLabel: textOrUndefined(branch.driverName) ?? "Unassigned", - driveTimeLabel: getRouteMetricLabel(formatRouteDurationSeconds(optimized?.metrics?.durationSeconds)), - id: `branch-${branch.id ?? index}`, - branchId: textOrUndefined(branch.id) ?? null, - routeKey: `branch:${textOrUndefined(branch.id) ?? index}`, - routePlanId: childDetail?.routePlanId ?? null, - isCurrent: false, - orderIds, - routeIndex, - status: formatRouteStatus(branch.status ?? childDetail?.routePlan?.status), - stops: branchStops, - stopsCount: branchStops.length, - title: textOrUndefined(branch.label) ?? `Route ${routeIndex}`, - totalDistanceLabel: getRouteMetricLabel(formatRouteDistanceMeters(optimized?.metrics?.distanceMeters)), - totalItems: ROUTE_EMPTY_LABEL, - totalWeightLabel: ROUTE_EMPTY_LABEL, - vehicleLabel: ROUTE_EMPTY_LABEL, - optimized, - }; - }); -} - function getRouteGroupChildOrderIds(child, detailStops, routeStops) { const explicitOrderIds = firstArray( child?.orderIds, @@ -1104,8 +1059,9 @@ function mapRouteChildDetailsByRoutePlanId(childRouteDetails = []) { } function buildRouteGroupChildRows(routeGroup, childDetailsByRoutePlanId = new Map(), routeStops = []) { - return getVisibleRouteGroupChildren(routeGroup).map((child, index) => { - const routeIndex = numberOrUndefined(child?.sortOrder) ?? index + 1; + const routeGroupChildRows = getVisibleRouteGroupChildren(routeGroup).map((child, index) => { + const routeIdx = numberOrUndefined(child?.routeIdx); + const routeIndex = routeIdx ?? numberOrUndefined(child?.sortOrder) ?? index + 1; const routePlanId = getRouteGroupChildRoutePlanId(child); const detail = childDetailsByRoutePlanId.get(routePlanId); const detailStops = detail?.stops ?? []; @@ -1122,7 +1078,6 @@ function buildRouteGroupChildRows(routeGroup, childDetailsByRoutePlanId = new Ma return { attemptedCount: countRouteStopsByStatus(stops, ["ATTEMPTED", "FAILED", "NEEDS_REVIEW"]), - branchId: null, 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, deliveredCount: countRouteStopsByStatus(stops, ["DELIVERED", "FULFILLED"]), @@ -1132,7 +1087,8 @@ function buildRouteGroupChildRows(routeGroup, childDetailsByRoutePlanId = new Ma isCurrent: false, optimized, orderIds: orderIds.length > 0 ? orderIds : stops.map((stop) => stop.orderId).filter(Boolean), - routeKey: routePlanId ? `routePlan:${routePlanId}` : `group-route:${index}`, + routeIdx: routeIdx ?? null, + routeKey: routePlanId ? `routePlan:${routePlanId}` : `routeIdx:${routeIndex}`, routeIndex, routePlanId: routePlanId ?? null, status: formatRouteStatus(detail?.routePlan?.status ?? child?.displayStatus ?? child?.status), @@ -1145,6 +1101,11 @@ function buildRouteGroupChildRows(routeGroup, childDetailsByRoutePlanId = new Ma vehicleLabel: getRouteVehicleLabel(detail?.routePlan), }; }).filter((routeRow) => routeRow.routePlanId); + routeGroupChildRows.sort((first, second) => ( + (numberOrUndefined(first.routeIdx) ?? numberOrUndefined(first.routeIndex) ?? 0) + - (numberOrUndefined(second.routeIdx) ?? numberOrUndefined(second.routeIndex) ?? 0) + )); + return routeGroupChildRows; } function applyRouteRowDraftState(routeRows, routeLineEdits, routePreviewByKey) { @@ -1196,13 +1157,14 @@ function ensureUniqueRouteRowColors(routeRows) { }); } -function getNextRouteBranchDraft(routeRows) { +function getNextChildRouteDraft(routeRows) { const usedColors = new Set(routeRows.map((routeRow) => normalizeRouteColor(routeRow.color)).filter(Boolean)); - const maxRouteIndex = routeRows.reduce((max, routeRow) => Math.max(max, numberOrUndefined(routeRow.routeIndex) ?? 0), 0); - const routeNumber = (maxRouteIndex || routeRows.length) + 1; + const maxRouteIdx = routeRows.reduce((max, routeRow) => Math.max(max, numberOrUndefined(routeRow.routeIdx) ?? numberOrUndefined(routeRow.routeIndex) ?? 0), 0); + const routeNumber = (maxRouteIdx || routeRows.length) + 1; return { color: getUnusedRouteColor(null, usedColors, routeNumber - 1), label: `Route ${routeNumber}`, + routeIdx: routeNumber, routeIndex: routeNumber, }; } @@ -1295,9 +1257,9 @@ function buildRouteGeometryRows(routeRows, childDetailsByRoutePlanId, fallbackRo function getRouteRowDraftKey(routeRow) { if (routeRow.routeKey) return routeRow.routeKey; - if (routeRow.isCurrent) return "root"; - if (routeRow.branchId) return `branch:${routeRow.branchId}`; + if (routeRow.routePlanId) return `routePlan:${routeRow.routePlanId}`; if (routeRow.tempId) return routeRow.tempId; + if (numberOrUndefined(routeRow.routeIdx) !== undefined) return `routeIdx:${routeRow.routeIdx}`; return routeRow.id; } @@ -1317,12 +1279,12 @@ function buildRouteDraftPayload(routeRows, { includeEmptyTempRoutes = true, incl routes: routeRows.filter((routeRow) => shouldIncludeRouteDraftRow(routeRow, includeEmptyTempRoutes)).map((routeRow, index) => { const optimized = getRouteDraftOptimized(routeRow, includeExistingOptimized); return { - branchId: routeRow.branchId ?? null, color: routeRow.color, label: routeRow.title, ...(optimized === undefined ? {} : { optimized }), orderIds: routeRow.stops.map((stop) => stop.orderId).filter(Boolean), routeKey: getRouteRowDraftKey(routeRow), + routeIdx: numberOrUndefined(routeRow.routeIdx) ?? numberOrUndefined(routeRow.routeIndex) ?? index + 1, routePlanId: routeRow.routePlanId ?? null, sortOrder: numberOrUndefined(routeRow.routeIndex) ?? index + 1, tempId: routeRow.tempId ?? null, @@ -1418,12 +1380,6 @@ export default function RouteDetailPage() { [childRouteDetails, orderedRouteStops, routeGroup], ); const routeGroupStopsSource = routeGroup ? allRouteGroupStops : orderedRouteStops; - const routeBranchRows = useMemo(() => buildRouteBranchRows(routeGroup, routeGroupStopsSource, routeChildDetailsByRoutePlanId), [routeGroupStopsSource, routeChildDetailsByRoutePlanId, routeGroup]); - const branchOrderIds = useMemo(() => new Set(routeBranchRows.flatMap((routeRow) => routeRow.orderIds)), [routeBranchRows]); - const rootRouteStops = useMemo( - () => routeGroupStopsSource.filter((stop) => !branchOrderIds.has(stop.orderId)), - [routeGroupStopsSource, branchOrderIds], - ); const routeGroupChildRows = useMemo(() => buildRouteGroupChildRows(routeGroup, routeChildDetailsByRoutePlanId, routeGroupStopsSource), [routeChildDetailsByRoutePlanId, routeGroup, routeGroupStopsSource]); const defaultRouteCandidateTitle = isRouteGroupDetail ? "Route 1" : routeDetailTitle; const routeStartDateTimeValue = getRouteStartDateTimeValue(effectiveRoutePlan); @@ -1490,7 +1446,6 @@ export default function RouteDetailPage() { : [ { attemptedCount: routeAttemptedCount, - branchId: null, color: routeLineColor, createdLabel: routeCreatedLabel, deliveredCount: routeDeliveredCount, @@ -1500,8 +1455,9 @@ export default function RouteDetailPage() { isCurrent: true, optimized: routeMetrics ? { metrics: routeMetrics, routeGeometry, routeStopPoints } : null, orderIds: orderedRouteStops.map((stop) => stop.orderId).filter(Boolean), - routeIndex: 1, - routeKey: "root", + routeIdx: numberOrUndefined(currentRouteGroupChild?.routeIdx) ?? 1, + routeIndex: numberOrUndefined(currentRouteGroupChild?.routeIdx) ?? 1, + routeKey: `routePlan:${textOrUndefined(effectiveRoutePlan?.id) ?? currentRouteLineId}`, routePlanId: textOrUndefined(effectiveRoutePlan?.id) ?? null, status: formatRouteStatus(effectiveRoutePlan?.status), stops: orderedRouteStops, @@ -1513,42 +1469,7 @@ export default function RouteDetailPage() { vehicleLabel: routeVehicleLabel, }, ]; - const groupRootRouteRows = isRouteGroupDetail - && rootRouteStops.length > 0 - ? [ - { - attemptedCount: countRouteStopsByStatus(rootRouteStops, ["ATTEMPTED", "FAILED", "NEEDS_REVIEW"]), - branchId: null, - color: routeLineColor, - createdLabel: ROUTE_EMPTY_LABEL, - deliveredCount: countRouteStopsByStatus(rootRouteStops, ["DELIVERED", "FULFILLED"]), - driverLabel: "Unassigned", - driveTimeLabel: ROUTE_EMPTY_LABEL, - id: "group-root-route", - isCurrent: false, - optimized: null, - orderIds: rootRouteStops.map((stop) => stop.orderId).filter(Boolean), - routeIndex: 1, - routeKey: "root", - routePlanId: null, - status: formatRouteStatus(routeGroup?.displayStatus ?? routeGroup?.status), - stops: rootRouteStops, - stopsCount: rootRouteStops.length, - title: "Route 1", - totalDistanceLabel: ROUTE_EMPTY_LABEL, - totalItems: getRouteTotalItems(null, rootRouteStops), - totalWeightLabel: ROUTE_EMPTY_LABEL, - vehicleLabel: ROUTE_EMPTY_LABEL, - }, - ] - : []; - const groupRouteRowsSource = isRouteGroupDetail - ? routeGroupChildRows.length > 0 - ? routeGroupChildRows - : [...groupRootRouteRows, ...routeBranchRows] - : routeGroupChildRows.length > 0 - ? routeGroupChildRows - : routeBranchRows; + const groupRouteRowsSource = routeGroupChildRows; const displayRouteRowsSource = isRouteGroupDetail ? groupRouteRowsSource : currentRouteRowsSource; const contextRouteRowsSource = isRouteGroupDetail ? groupRouteRowsSource @@ -1929,13 +1850,12 @@ export default function RouteDetailPage() { }, []); const handleAddEmptyRoute = () => { - const draft = getNextRouteBranchDraft(routeRows); + const draft = getNextChildRouteDraft(contextRouteRows); const tempId = `temp:${Date.now()}-${Math.random().toString(16).slice(2)}`; setClientRouteRows((rows) => [ ...rows, { attemptedCount: 0, - branchId: null, color: draft.color, createdLabel: ROUTE_EMPTY_LABEL, deliveredCount: 0, @@ -1945,6 +1865,7 @@ export default function RouteDetailPage() { isCurrent: false, orderIds: [], routeKey: tempId, + routeIdx: draft.routeIdx, routeIndex: draft.routeIndex, routePlanId: null, stops: [], @@ -1968,7 +1889,7 @@ export default function RouteDetailPage() { const handleSaveRouteDraft = () => { if (!canSaveRouteDraft) return; submitRouteGroupAction("saveRouteDraft", { - draft: JSON.stringify(buildRouteDraftPayload(contextTimelineRouteRows, { includeEmptyTempRoutes: false, includeExistingOptimized: false })), + draft: JSON.stringify(buildRouteDraftPayload(contextTimelineRouteRows, { includeExistingOptimized: false })), }); }; diff --git a/apps/shopify-app/tests/route-groups-helper.test.mjs b/apps/shopify-app/tests/route-groups-helper.test.mjs index 84c4395..72d2aff 100644 --- a/apps/shopify-app/tests/route-groups-helper.test.mjs +++ b/apps/shopify-app/tests/route-groups-helper.test.mjs @@ -131,7 +131,7 @@ test("route group helper updates membership without child generation side effect test("route group helper saves a batched draft allocation", async () => { const fakeFetch = makeFetch({ data: { routeGroup: { id: "group/1", status: "DRAFT" } }, error: null }); - const payload = { routes: [{ branchId: null, orderIds: ["order-1"] }, { branchId: "branch/1", orderIds: ["order-2"] }] }; + const payload = { routes: [{ orderIds: ["order-1"], routeIdx: 1, routePlanId: "route/1" }, { orderIds: ["order-2"], routeIdx: 2, tempId: "temp/2" }] }; const result = await saveDeliveryRouteGroupDraft(makeRequest(), "group/1", payload, { fetch: fakeFetch, @@ -145,9 +145,9 @@ test("route group helper saves a batched draft allocation", async () => { }); test("route group helper previews optimization without saving the draft", async () => { - const preview = { routes: [{ orderIds: ["order-1"], routeKey: "root" }] }; + const preview = { routes: [{ orderIds: ["order-1"], routeIdx: 1, routeKey: "routeIdx:1" }] }; const fakeFetch = makeFetch({ data: { preview }, error: null }); - const payload = { mode: "OPTIMIZE_ORDER", routes: [{ branchId: null, orderIds: ["order-1"], routeKey: "root" }] }; + const payload = { mode: "OPTIMIZE_ORDER", routes: [{ orderIds: ["order-1"], routeIdx: 1, routeKey: "routeIdx:1" }] }; const result = await previewDeliveryRouteGroupOptimization(makeRequest(), "group/1", payload, { fetch: fakeFetch, diff --git a/apps/shopify-app/tests/routes-page.test.mjs b/apps/shopify-app/tests/routes-page.test.mjs index 7247eb2..7ba21a6 100644 --- a/apps/shopify-app/tests/routes-page.test.mjs +++ b/apps/shopify-app/tests/routes-page.test.mjs @@ -74,7 +74,8 @@ test("Routes page lists saved child routes below their parent route group", () = assert.match(routeHelpersSource, /name\.startsWith\(`\$\{groupName\} — `\)/); assert.match(routesPageSource, /route: getRouteGroupChildRouteName\(routeGroup, child, routePlan, index\)/); assert.match(routesPageSource, /parentRouteGroupId: routeGroup\.id/); - assert.match(routeHelpersSource, /return children\.length >= 2 \? children : \[\]/); + assert.doesNotMatch(routeHelpersSource, /return children\.length >= 2 \? children : \[\]/); + assert.match(routeHelpersSource, /leftRouteIdx = numberOrUndefined\(left\.child\?\.routeIdx\)/); assert.match(routesPageSource, /isDeletable: true,[\s\S]*deleteKey: `routePlan:\$\{routePlanId\}`/); assert.match(routesPageSource, /return \[\.\.\.routeGroupRows, \.\.\.routeChildRows, \.\.\.routePlanRows\]/); }); @@ -172,7 +173,8 @@ test("Routes table uses aligned CLEVER planning columns", () => { assert.match(routesPageSource, /const routeGroupRows = safeRouteGroups\.map\(\(routeGroup\) =>/); assert.match(routesPageSource, /function getRouteGroupTotalOrders\(routeGroup\)/); assert.match(routesPageSource, /return Number\(routeGroup\?\.totalOrders \?\? routeGroup\?\.ordersCount \?\? routeGroup\?\.assignments\?\.length \?\? 0\) \|\| 0/); - assert.match(routeHelpersSource, /return children\.length >= 2 \? children : \[\]/); + 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, /isRouteGroup: true/); @@ -321,7 +323,7 @@ test("Route detail wires route group action buttons through App Bridge", () => { assert.match(routeDetailSource, /\{reOptimizeRouteGroupBusy \? "Working…" : "Re-optimize"\}/); assert.match(routeDetailSource, /\{addEmptyRouteBranchBusy \? "Working…" : "Add Empty Route"\}/); assert.match(routeDetailSource, /submitRouteGroupAction\("previewRouteOptimization", \{\s+draft: JSON\.stringify\(buildRouteDraftPayload\(contextTimelineRouteRows, \{ includeExistingOptimized: true \}\)\),/); - assert.match(routeDetailSource, /submitRouteGroupAction\("saveRouteDraft", \{\s+draft: JSON\.stringify\(buildRouteDraftPayload\(contextTimelineRouteRows, \{ includeEmptyTempRoutes: false, includeExistingOptimized: false \}\)\),/); + assert.match(routeDetailSource, /submitRouteGroupAction\("saveRouteDraft", \{\s+draft: JSON\.stringify\(buildRouteDraftPayload\(contextTimelineRouteRows, \{ includeExistingOptimized: false \}\)\),/); assert.match(routeDetailSource, /const handleAddEmptyRoute = \(\) => \{/); assert.match(routeDetailSource, /setClientRouteRows\(\(rows\) => \[/); assert.match(routeDetailSource, /const polygonCandidateOrderIds = polygonCandidateStops\.map\(\(stop\) => stop\.orderId\)/); @@ -741,6 +743,32 @@ test("Route detail marker rendering does not call MapLibre resize from map event assert.doesNotMatch(routeDetailSource, /\.on\("zoomend", syncRouteDetailMapLayers\)/); }); + +test("Route detail uses child-only rows and global routeIdx save assertions", () => { + assert.doesNotMatch(routeHelpersSource, /return children\.length >= 2 \? children : \[\]/); + assert.match(routeHelpersSource, /routeIdx/); + assert.doesNotMatch(routeDetailSource, /function buildRouteBranchRows\(/); + assert.doesNotMatch(routeDetailSource, /buildRouteBranchRows\(routeGroup/); + assert.doesNotMatch(routeDetailSource, /groupRootRouteRows/); + assert.doesNotMatch(routeDetailSource, /branchOrderIds/); + assert.doesNotMatch(routeDetailSource, /rootRouteStops/); + assert.match(routeDetailSource, /routeGroupChildRows\.sort/); + assert.match(routeDetailSource, /routeIdx/); +}); + +test("Route detail draft payload is child-only and treats routeIdx as server assertion", () => { + const start = routeDetailSource.indexOf("function buildRouteDraftPayload("); + const end = routeDetailSource.indexOf("function renderRouteHeaderMetric", start); + const payloadBuilder = routeDetailSource.slice(start, end); + + assert.match(payloadBuilder, /routeIdx:/); + assert.match(payloadBuilder, /routePlanId: routeRow\.routePlanId \?\? null/); + assert.match(payloadBuilder, /tempId: routeRow\.tempId \?\? null/); + assert.doesNotMatch(payloadBuilder, /branchId:/); + assert.doesNotMatch(routeDetailSource, /routeKey: "root"/); + assert.doesNotMatch(routeDetailSource, /if \(routeRow\.isCurrent\) return "root"/); +}); + test("Route detail renders route lines and a stop timeline below the map", () => { assert.match(routeDetailSource, /function logRouteDetailPerformance\(name, metric = \{\}\) \{/); assert.match(routeDetailServerSource, /routes\.detail\.action\.saveRouteDraft\.request/); @@ -751,19 +779,21 @@ test("Route detail renders route lines and a stop timeline below the map", () => assert.match(routeDetailSource, /function buildRouteGroupStops\(routeGroup, childRouteDetails, currentRouteStops\) \{/); assert.match(routeDetailSource, /const assignmentStops = buildRouteStops\(routeGroup\?\.assignments \?\? \[\]\)/); assert.match(routeDetailSource, /const allRouteGroupStops = useMemo/); - assert.match(routeDetailSource, /buildRouteBranchRows\(routeGroup, routeGroupStopsSource, routeChildDetailsByRoutePlanId\)/); - assert.match(routeDetailSource, /routeGroupStopsSource\.filter\(\(stop\) => !branchOrderIds\.has\(stop\.orderId\)\)/); assert.match(routeDetailSource, /const routePlanRowsColumnWidths = \[/); - assert.match(routeDetailSource, /function buildRouteBranchRows\(routeGroup, routeStops = \[\], childDetailsByRoutePlanId = new Map\(\)\) \{/); - assert.match(routeDetailSource, /readRouteOptimizedSnapshot\(branch\.optimized\)/); + assert.match(routeDetailSource, /function buildRouteGroupChildRows\(routeGroup, childDetailsByRoutePlanId = new Map\(\), routeStops = \[\]\) \{/); + assert.match(routeDetailSource, /getVisibleRouteGroupChildren\(routeGroup\)\.map/); + assert.match(routeDetailSource, /const routeIdx = numberOrUndefined\(child\?\.routeIdx\)/); + assert.match(routeDetailSource, /routeIdx: routeIdx \?\? null/); + assert.match(routeDetailSource, /routeGroupChildRows\.sort/); + assert.doesNotMatch(routeDetailSource, /function buildRouteBranchRows\(/); + assert.doesNotMatch(routeDetailSource, /rootRouteStops/); + assert.doesNotMatch(routeDetailSource, /groupRootRouteRows/); + assert.doesNotMatch(routeDetailSource, /routeBranchRows/); assert.match(routeDetailSource, /formatRouteDurationSeconds\(optimized\?\.metrics\?\.durationSeconds\)/); assert.match(routeDetailSource, /formatRouteDistanceMeters\(optimized\?\.metrics\?\.distanceMeters\)/); - assert.match(routeDetailSource, /const childDetail = childDetailsByRoutePlanId\.get\(textOrUndefined\(branch\.routePlanId\)\)/); - assert.match(routeDetailSource, /const rootRouteStops = useMemo/); - assert.match(routeHelpersSource, /return children\.length >= 2 \? children : \[\]/); - assert.match(routeDetailSource, /const groupRootRouteRows = isRouteGroupDetail\s*&& rootRouteStops\.length > 0/); - assert.match(routeDetailSource, /\? routeGroupChildRows\.length > 0[\s\S]*\? routeGroupChildRows[\s\S]*: \[\.\.\.groupRootRouteRows, \.\.\.routeBranchRows\]/); - assert.match(routeDetailSource, /const maxRouteIndex = routeRows\.reduce/); + assert.match(routeDetailSource, /const maxRouteIdx = routeRows\.reduce/); + assert.match(routeDetailSource, /const draft = getNextChildRouteDraft\(contextRouteRows\)/); + assert.match(routeDetailSource, /routeIdx: draft\.routeIdx/); assert.match(routeDetailSource, /routeIndex: draft\.routeIndex/); assert.match(routeHelpersSource, /getDefaultRouteGroupChildName\(index, child\)/); assert.match(routeDetailSource, /const routePolygonSourceStops = timelineRouteRows\.length > 0[\s\S]*: isRouteGroupDetail \? routeGroupStopsSource : \[\]/); @@ -776,7 +806,7 @@ test("Route detail renders route lines and a stop timeline below the map", () => assert.match(routeDetailSource, /if \(routeRow\.routePlanId && !includeExistingOptimized\) return undefined/); assert.match(routeDetailSource, /function shouldIncludeRouteDraftRow\(routeRow, includeEmptyTempRoutes\) \{/); assert.match(routeDetailSource, /return !\(routeRow\.tempId && !routeRow\.routePlanId && routeRow\.stops\.length === 0\)/); - assert.match(routeDetailSource, /buildRouteDraftPayload\(contextTimelineRouteRows, \{ includeEmptyTempRoutes: false, includeExistingOptimized: false \}\)/); + assert.match(routeDetailSource, /buildRouteDraftPayload\(contextTimelineRouteRows, \{ includeExistingOptimized: false \}\)/); assert.ok( routeDetailSource.indexOf("const [routeCandidateTitle") < routeDetailSource.indexOf("const currentRouteRowsSource ="), "routeRows reads route line state after the state is initialized", @@ -796,8 +826,7 @@ 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 \? "Route 1" : routeDetailTitle/); - assert.match(routeDetailSource, /const routeIndex = Math\.max\(numberOrUndefined\(branch\.sortOrder\) \?\? index \+ 2, index \+ 2\)/); - assert.match(routeDetailSource, /title: textOrUndefined\(branch\.label\) \?\? `Route \$\{routeIndex\}`/); + assert.match(routeDetailSource, /title: getRouteGroupChildRouteName\(routeGroup, child, detail\?\.routePlan \?\? child\?\.routePlan, index\)/); 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"/); @@ -844,7 +873,7 @@ test("Route detail renders route lines and a stop timeline below the map", () => assert.doesNotMatch(routeDetailSource, /strokeWidth="2\.2"/); assert.match(routeDetailSource, /function ensureUniqueRouteRowColors\(routeRows\) \{/); assert.match(routeDetailSource, /const ROUTE_DEFAULT_COLORS = \[MAP_MARKER_PALETTE\.plannedOrder\.color/); - assert.match(routeDetailSource, /ROUTE_DEFAULT_COLORS\[\(index \+ 1\) % ROUTE_DEFAULT_COLORS\.length\]/); + assert.match(routeDetailSource, /ROUTE_DEFAULT_COLORS\[index % ROUTE_DEFAULT_COLORS\.length\]/); assert.match(routeDetailSource, /ROUTE_COLOR_OPTIONS\.map\(\(color\) =>/); assert.match(routeDetailSource, /function getUnusedRouteColor\(preferredColor, usedColors/); assert.match(routeDetailSource, /aria-label="Route color picker"/);