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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 10 additions & 2 deletions apps/shopify-app/app/features/delivery/route-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 = "-") {
Expand Down
127 changes: 24 additions & 103 deletions apps/shopify-app/app/routes/app.routes.$routeId.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 ?? [];
Expand All @@ -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"]),
Expand All @@ -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),
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
};
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1490,7 +1446,6 @@ export default function RouteDetailPage() {
: [
{
attemptedCount: routeAttemptedCount,
branchId: null,
color: routeLineColor,
createdLabel: routeCreatedLabel,
deliveredCount: routeDeliveredCount,
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -1945,6 +1865,7 @@ export default function RouteDetailPage() {
isCurrent: false,
orderIds: [],
routeKey: tempId,
routeIdx: draft.routeIdx,
routeIndex: draft.routeIndex,
routePlanId: null,
stops: [],
Expand All @@ -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 })),
});
};

Expand Down
6 changes: 3 additions & 3 deletions apps/shopify-app/tests/route-groups-helper.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading