Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 65 additions & 10 deletions apps/shopify-app/app/features/delivery/route-detail-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) ??
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1023,6 +1077,7 @@ function getRouteStopFromMapFeature(feature, routeStops) {
}

export {
buildRouteDetailMarkerFeatureCollection,
buildRouteDetailLiveTrackingData,
DEFAULT_CENTER,
ROUTE_DETAIL_COMPLETED_STOP_COLOR,
Expand Down
19 changes: 10 additions & 9 deletions apps/shopify-app/app/routes/app.routes.$routeId.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ const routeTrackingMapGpsKeyStyle = {
const routeTrackingMapReferenceKeyStyle = {
borderRadius: "999px",
height: "3px",
opacity: 0.22,
opacity: 0.42,
width: "22px",
};

Expand Down Expand Up @@ -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,
})),
);
Expand All @@ -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),
Expand Down
62 changes: 60 additions & 2 deletions apps/shopify-app/tests/route-detail-map-behavior.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 10 additions & 2 deletions apps/shopify-app/tests/route-tracking-live.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
Expand All @@ -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\)/);
Expand Down