From 587492e51f05d66d3c3aad4b1063bf3224277c59 Mon Sep 17 00:00:00 2001 From: OziinG <145884442+OziinG@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:00:19 +0900 Subject: [PATCH] Keep tracking completion clear without muting route context Completed stops retain their route-colored pins and gain a dedicated Tracking-only check badge, while the planned route remains a visible translucent solid reference beneath dashed GPS. Constraint: Preserve the shared Stops/Tracking map instance and server-owned completion state. Rejected: Dimming or graying completed Tracking pins | it hides route identity and made markers appear disabled. Confidence: high Scope-risk: narrow Directive: Keep planned route solid and actual GPS dashed; completion state belongs in the badge, not marker opacity. Tested: 351 Shopify app tests; targeted map behavior tests; typecheck; production build; public URL guard; changed-file ESLint; diff check. Not-tested: Authenticated Shopify Admin visual smoke before deployment. --- .../app/features/delivery/route-detail-map.js | 75 ++++++++++++++++--- .../app/routes/app.routes.$routeId.jsx | 19 ++--- .../tests/route-detail-map-behavior.test.mjs | 62 ++++++++++++++- .../tests/route-tracking-live.test.mjs | 12 ++- 4 files changed, 145 insertions(+), 23 deletions(-) diff --git a/apps/shopify-app/app/features/delivery/route-detail-map.js b/apps/shopify-app/app/features/delivery/route-detail-map.js index ee6c037..36a2de5 100644 --- a/apps/shopify-app/app/features/delivery/route-detail-map.js +++ b/apps/shopify-app/app/features/delivery/route-detail-map.js @@ -8,6 +8,8 @@ const ROUTE_DETAIL_ROUTE_LAYER_ID = "route-detail-osrm-route-line"; const ROUTE_DETAIL_MARKER_SOURCE_ID = "route-detail-markers"; const ROUTE_DETAIL_DEPARTURE_LAYER_ID = "route-detail-departure-marker"; const ROUTE_DETAIL_STOP_LAYER_ID = "route-detail-stop-markers"; +const ROUTE_DETAIL_STOP_COMPLETION_BADGE_LAYER_ID = "route-detail-stop-completion-badges"; +const ROUTE_DETAIL_STOP_COMPLETION_CHECK_LAYER_ID = "route-detail-stop-completion-checks"; const ROUTE_DETAIL_STOP_POINT_SOURCE_ID = "route-detail-snapped-stop-points"; const ROUTE_DETAIL_STOP_POINT_LAYER_ID = "route-detail-snapped-stop-points"; const ROUTE_DETAIL_TRACKING_SOURCE_ID = "route-detail-live-tracking"; @@ -263,8 +265,8 @@ function syncRouteDetailRouteLine(map, routeLines, routeColor = "#e11900", optio } const existingSource = map.getSource?.(ROUTE_DETAIL_ROUTE_SOURCE_ID); - const routeLineOpacity = options.isTrackingReference ? 0.22 : 0.78; - const routeLineWidth = 2.5; + const routeLineOpacity = options.isTrackingReference ? 0.42 : 0.78; + const routeLineWidth = options.isTrackingReference ? 3.5 : 2.5; if (existingSource?.setData) { existingSource.setData(routeLineData); } else { @@ -512,18 +514,24 @@ function syncRouteDetailTrackingVisibility(map, isTrackingView = false) { function syncRouteDetailMapViewEmphasis(map, isTrackingView = false) { if (!isRouteDetailMapStyleReady(map)) return false; - const stopOpacity = isTrackingView ? 0.42 : 1; - const departureOpacity = isTrackingView ? 0.65 : 1; - const stopPointOpacity = isTrackingView ? 0.3 : 1; if (map.getLayer?.(ROUTE_DETAIL_STOP_LAYER_ID)) { - map.setPaintProperty?.(ROUTE_DETAIL_STOP_LAYER_ID, "icon-opacity", stopOpacity); + map.setPaintProperty?.(ROUTE_DETAIL_STOP_LAYER_ID, "icon-opacity", 1); } if (map.getLayer?.(ROUTE_DETAIL_DEPARTURE_LAYER_ID)) { - map.setPaintProperty?.(ROUTE_DETAIL_DEPARTURE_LAYER_ID, "icon-opacity", departureOpacity); + map.setPaintProperty?.(ROUTE_DETAIL_DEPARTURE_LAYER_ID, "icon-opacity", 1); } if (map.getLayer?.(ROUTE_DETAIL_STOP_POINT_LAYER_ID)) { - map.setPaintProperty?.(ROUTE_DETAIL_STOP_POINT_LAYER_ID, "circle-opacity", stopPointOpacity); - map.setPaintProperty?.(ROUTE_DETAIL_STOP_POINT_LAYER_ID, "circle-stroke-opacity", stopPointOpacity); + map.setPaintProperty?.(ROUTE_DETAIL_STOP_POINT_LAYER_ID, "circle-opacity", 1); + map.setPaintProperty?.(ROUTE_DETAIL_STOP_POINT_LAYER_ID, "circle-stroke-opacity", 1); + } + const completionVisibility = isTrackingView ? "visible" : "none"; + for (const layerId of [ + ROUTE_DETAIL_STOP_COMPLETION_BADGE_LAYER_ID, + ROUTE_DETAIL_STOP_COMPLETION_CHECK_LAYER_ID, + ]) { + if (map.getLayer?.(layerId)) { + map.setLayoutProperty?.(layerId, "visibility", completionVisibility); + } } return true; } @@ -775,7 +783,7 @@ function fitRouteStopAndSnappedPoint(map, maplibregl, stop, routeStopPoint) { } function getRouteStopDisplayColor(stop, routeColor, routeStopColorById) { - if (isRouteStopCompleted(stop)) return ROUTE_DETAIL_COMPLETED_STOP_COLOR; + if (isRouteStopCompleted(stop) && !stop.preserveRouteColor) return ROUTE_DETAIL_COMPLETED_STOP_COLOR; return ( routeStopColorById?.get(stop.id) ?? routeStopColorById?.get(stop.deliveryStopId) ?? @@ -866,6 +874,7 @@ function buildRouteDetailMarkerFeatureCollection(departureLocation, routeStops, }, properties: { featureType: "routeStop", + isCompleted: Boolean(stop.isTrackingCompleted || isRouteStopCompleted(stop)), orderId: stop.orderId ?? "", pinImage: getRouteDetailStopPinImageId(stop, stopColor), sortKey: stop.isPolygonSelected ? 3000 : 1000 - stop.stop, @@ -965,6 +974,51 @@ function syncRouteDetailMapMarkerLayers(map, departureLocation, routeStops, rout map.setFilter?.(ROUTE_DETAIL_STOP_LAYER_ID, ["==", ["get", "featureType"], "routeStop"]); } + const completedStopFilter = [ + "all", + ["==", ["get", "featureType"], "routeStop"], + ["==", ["get", "isCompleted"], true], + ]; + if (!map.getLayer?.(ROUTE_DETAIL_STOP_COMPLETION_BADGE_LAYER_ID)) { + map.addLayer({ + id: ROUTE_DETAIL_STOP_COMPLETION_BADGE_LAYER_ID, + type: "circle", + source: ROUTE_DETAIL_MARKER_SOURCE_ID, + filter: completedStopFilter, + layout: { + visibility: "none", + }, + paint: { + "circle-color": "#008060", + "circle-radius": 7, + "circle-stroke-color": "#ffffff", + "circle-stroke-width": 2, + "circle-translate": [-8, -23], + "circle-translate-anchor": "viewport", + }, + }); + } + if (!map.getLayer?.(ROUTE_DETAIL_STOP_COMPLETION_CHECK_LAYER_ID)) { + map.addLayer({ + id: ROUTE_DETAIL_STOP_COMPLETION_CHECK_LAYER_ID, + type: "symbol", + source: ROUTE_DETAIL_MARKER_SOURCE_ID, + filter: completedStopFilter, + layout: { + "text-allow-overlap": true, + "text-field": "✓", + "text-ignore-placement": true, + "text-size": 10, + visibility: "none", + }, + paint: { + "text-color": "#ffffff", + "text-translate": [-8, -23], + "text-translate-anchor": "viewport", + }, + }); + } + const existingStopPointSource = map.getSource?.(ROUTE_DETAIL_STOP_POINT_SOURCE_ID); if (existingStopPointSource?.setData) { existingStopPointSource.setData(stopPointData); @@ -1023,6 +1077,7 @@ function getRouteStopFromMapFeature(feature, routeStops) { } export { + buildRouteDetailMarkerFeatureCollection, buildRouteDetailLiveTrackingData, DEFAULT_CENTER, ROUTE_DETAIL_COMPLETED_STOP_COLOR, diff --git a/apps/shopify-app/app/routes/app.routes.$routeId.jsx b/apps/shopify-app/app/routes/app.routes.$routeId.jsx index 3aa0e8c..8f5153d 100644 --- a/apps/shopify-app/app/routes/app.routes.$routeId.jsx +++ b/apps/shopify-app/app/routes/app.routes.$routeId.jsx @@ -585,7 +585,7 @@ const routeTrackingMapGpsKeyStyle = { const routeTrackingMapReferenceKeyStyle = { borderRadius: "999px", height: "3px", - opacity: 0.22, + opacity: 0.42, width: "22px", }; @@ -2743,23 +2743,22 @@ export default function RouteDetailPage() { }, [routeTrackingProgress?.completedStopIds, timelineRouteRows]); const routeStopColorById = useMemo(() => new Map(timelineRouteRows.flatMap((routeRow) => ( routeRow.stops.flatMap((stop) => { - const isCompleted = completedTrackingStopIds.has(stop.id) - || completedTrackingStopIds.has(stop.deliveryStopId); - const stopColor = isCompleted ? ROUTE_DETAIL_COMPLETED_STOP_COLOR : routeRow.color; return [ - [stop.id, stopColor], - ...(stop.deliveryStopId ? [[stop.deliveryStopId, stopColor]] : []), - ...(stop.orderId ? [[stop.orderId, stopColor]] : []), + [stop.id, routeRow.color], + ...(stop.deliveryStopId ? [[stop.deliveryStopId, routeRow.color]] : []), + ...(stop.orderId ? [[stop.orderId, routeRow.color]] : []), ]; }) - ))), [completedTrackingStopIds, timelineRouteRows]); + ))), [timelineRouteRows]); const trackingDeliveredCount = childRouteOrderRows.filter((row) => completedTrackingStopIds.has(row.id)).length; const routeMapStops = useMemo(() => { if (timelineRouteRows.length > 0) { return timelineRouteRows.flatMap((routeRow) => routeRow.stops.map((stop) => ({ ...stop, + isTrackingCompleted: completedTrackingStopIds.has(stop.id) || completedTrackingStopIds.has(stop.deliveryStopId), isPolygonSelected: polygonHighlightedOrderIds.has(stop.orderId), + preserveRouteColor: isTrackingMapView, routeColor: routeStopColorById.get(stop.id) ?? routeRow.color, })), ); @@ -2768,11 +2767,13 @@ export default function RouteDetailPage() { return isRouteGroupDetail ? routeGroupStopsSource.map((stop) => ({ ...stop, + isTrackingCompleted: completedTrackingStopIds.has(stop.id) || completedTrackingStopIds.has(stop.deliveryStopId), isPolygonSelected: polygonHighlightedOrderIds.has(stop.orderId), + preserveRouteColor: isTrackingMapView, routeColor: routeLineColor, })) : []; - }, [isRouteGroupDetail, polygonHighlightedOrderIds, routeGroupStopsSource, routeLineColor, routeStopColorById, timelineRouteRows]); + }, [completedTrackingStopIds, isRouteGroupDetail, isTrackingMapView, polygonHighlightedOrderIds, routeGroupStopsSource, routeLineColor, routeStopColorById, timelineRouteRows]); const routeMapLocationsSource = routeMapStops.length > 0 ? routeMapStops : orderedRouteStops; const routeMapCenter = useMemo( () => getRouteMapCenter(departureLocation, routeMapLocationsSource), diff --git a/apps/shopify-app/tests/route-detail-map-behavior.test.mjs b/apps/shopify-app/tests/route-detail-map-behavior.test.mjs index f9242b3..4840f9d 100644 --- a/apps/shopify-app/tests/route-detail-map-behavior.test.mjs +++ b/apps/shopify-app/tests/route-detail-map-behavior.test.mjs @@ -2,10 +2,12 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + buildRouteDetailMarkerFeatureCollection, getRouteDetailPopupPanOffset, getRouteDetailTrackingArrivalItems, getRouteTrackingArrivalListMaxHeight, syncRouteDetailLiveTracking, + syncRouteDetailMapViewEmphasis, syncRouteDetailRouteLine, syncRouteDetailTrackingVisibility, } from "../app/features/delivery/route-detail-map.js"; @@ -20,7 +22,7 @@ const TRACKING_LAYER_IDS = [ function createFakeMap() { const sources = new Map(); const layers = new Map(); - const calls = { addLayer: [], addSource: [], setLayoutProperty: [] }; + const calls = { addLayer: [], addSource: [], setLayoutProperty: [], setPaintProperty: [] }; const map = { addLayer(layer) { calls.addLayer.push(layer.id); @@ -49,11 +51,67 @@ function createFakeMap() { const layer = layers.get(id); if (layer) layer.layout = { ...layer.layout, [property]: value }; }, - setPaintProperty() {}, + setPaintProperty(id, property, value) { + calls.setPaintProperty.push([id, property, value]); + const layer = layers.get(id); + if (layer) layer.paint = { ...layer.paint, [property]: value }; + }, }; return { calls, layers, map, sources }; } +test("Tracking keeps planned stops opaque and shows completion checks only in Tracking", () => { + const fake = createFakeMap(); + fake.map.addLayer({ id: "route-detail-stop-markers", type: "symbol", paint: {} }); + fake.map.addLayer({ id: "route-detail-departure-marker", type: "symbol", paint: {} }); + fake.map.addLayer({ id: "route-detail-snapped-stop-points", type: "circle", paint: {} }); + fake.map.addLayer({ id: "route-detail-stop-completion-badges", type: "circle", layout: {} }); + fake.map.addLayer({ id: "route-detail-stop-completion-checks", type: "symbol", layout: {} }); + + assert.equal(syncRouteDetailMapViewEmphasis(fake.map, true), true); + assert.equal(fake.layers.get("route-detail-stop-markers").paint["icon-opacity"], 1); + assert.equal(fake.layers.get("route-detail-departure-marker").paint["icon-opacity"], 1); + assert.equal(fake.layers.get("route-detail-snapped-stop-points").paint["circle-opacity"], 1); + assert.equal(fake.layers.get("route-detail-stop-completion-badges").layout.visibility, "visible"); + assert.equal(fake.layers.get("route-detail-stop-completion-checks").layout.visibility, "visible"); + + assert.equal(syncRouteDetailMapViewEmphasis(fake.map, false), true); + assert.equal(fake.layers.get("route-detail-stop-completion-badges").layout.visibility, "none"); + assert.equal(fake.layers.get("route-detail-stop-completion-checks").layout.visibility, "none"); +}); + +test("Tracking completion preserves the route-colored marker and exposes badge state", () => { + const markerData = buildRouteDetailMarkerFeatureCollection(null, [{ + coordinates: [126.927, 37.512], + deliveryStopId: "stop-3", + hasCoordinates: true, + id: "order-3", + isTrackingCompleted: true, + preserveRouteColor: true, + routeColor: "#006fbb", + status: "DELIVERED", + stop: 3, + }], [], "#e11900", new Map()); + + assert.equal(markerData.features.length, 1); + assert.equal(markerData.features[0].properties.isCompleted, true); + assert.equal(markerData.features[0].properties.pinImage, "route-detail-stop-pin-006fbb-3"); +}); + +test("Tracking planned route remains a solid, visible reference under dashed GPS", () => { + const fake = createFakeMap(); + const routeGeometry = { + coordinates: [[126.92, 37.51], [126.93, 37.52]], + type: "LineString", + }; + + assert.equal(syncRouteDetailRouteLine(fake.map, routeGeometry, "#006fbb", { isTrackingReference: true }), true); + const routeLayer = fake.layers.get("route-detail-osrm-route-line"); + assert.equal(routeLayer.paint["line-opacity"], 0.42); + assert.equal(routeLayer.paint["line-width"], 3.5); + assert.equal(routeLayer.paint["line-dasharray"], undefined); +}); + test("arrival popup sizing stays inside the visible tracking map viewport", () => { assert.equal(getRouteTrackingArrivalListMaxHeight(520), 260); assert.equal(getRouteTrackingArrivalListMaxHeight(240), 138); diff --git a/apps/shopify-app/tests/route-tracking-live.test.mjs b/apps/shopify-app/tests/route-tracking-live.test.mjs index 1838dca..82fb269 100644 --- a/apps/shopify-app/tests/route-tracking-live.test.mjs +++ b/apps/shopify-app/tests/route-tracking-live.test.mjs @@ -117,8 +117,8 @@ test("live tracking updates MapLibre sources instead of rebuilding the child map assert.match(routeMapSource, /trackingTrail/); assert.match(routeMapSource, /trackingConnector/); assert.match(routeMapSource, /getRouteTrackingLineFeatures/); - assert.match(routeMapSource, /const routeLineOpacity = options\.isTrackingReference \? 0\.22 : 0\.78/); - assert.match(routeMapSource, /const routeLineWidth = 2\.5/); + assert.match(routeMapSource, /const routeLineOpacity = options\.isTrackingReference \? 0\.42 : 0\.78/); + assert.match(routeMapSource, /const routeLineWidth = options\.isTrackingReference \? 3\.5 : 2\.5/); assert.equal((routeMapSource.match(/"line-dasharray": \[1\.5, 1\.25\]/g) ?? []).length, 2); assert.doesNotMatch(routeMapSource, /"line-width": 4\.5/); assert.match(routeMapSource, /isTrackingReference/); @@ -130,7 +130,15 @@ test("live tracking updates MapLibre sources instead of rebuilding the child map assert.match(routeMapSource, /arrivalDetailsJson/); assert.doesNotMatch(routeMapSource, /badgeOffset|labelOffset|ARRIVAL_BADGE_IMAGE_ID/); assert.match(routeMapSource, /ROUTE_DETAIL_COMPLETED_STOP_COLOR/); + assert.match(routeMapSource, /const ROUTE_DETAIL_STOP_COMPLETION_BADGE_LAYER_ID/); + assert.match(routeMapSource, /const ROUTE_DETAIL_STOP_COMPLETION_CHECK_LAYER_ID/); + assert.match(routeMapSource, /isCompleted: Boolean\(stop\.isTrackingCompleted \|\| isRouteStopCompleted\(stop\)\)/); + assert.match(routeMapSource, /"circle-translate": \[-8, -23\]/); + assert.match(routeMapSource, /"text-field": "✓"/); + assert.doesNotMatch(routeMapSource, /const stopOpacity = isTrackingView \? 0\.42 : 1/); assert.match(routeDetailSource, /completedTrackingStopIds/); + assert.match(routeDetailSource, /isTrackingCompleted: completedTrackingStopIds\.has\(stop\.id\)/); + assert.match(routeDetailSource, /preserveRouteColor: isTrackingMapView/); assert.match(routeDetailSource, /if \(!isTrackingMapView \|\| !isMapReady \|\| !routeMapRef\.current\) return undefined/); assert.match(routeDetailSource, /syncRouteDetailLiveTracking\(routeMapRef\.current, displayedRouteTrackingSnapshot, routeMapStops\)/); assert.match(routeDetailSource, /map\.on\("click", ROUTE_DETAIL_TRACKING_ARRIVAL_CIRCLE_LAYER_ID, handleArrivalMarkerClick\)/);