From 75e270de321ad15c1935f616204bcfe1cdcb3b1e Mon Sep 17 00:00:00 2001 From: OziinG <145884442+OziinG@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:05:03 +0900 Subject: [PATCH] Make concrete delivery batches directly filterable Expose counted delivery-date options, including unresolved dates, without changing the delivery calculation or server contract. Constraint: Keep the Orders page as one surface and reuse the existing filter row. Rejected: Reintroducing shift-range selection | Selection does not define the operational delivery batch. Confidence: high Scope-risk: narrow Tested: npm test; npm run check:public-urls; app typecheck; app build; targeted Orders tests Not-tested: Live embedded Shopify visual check --- .../app/features/orders/order-filters.js | 43 +++++++++++++++++-- .../app/features/orders/order-filters.test.js | 42 ++++++++++++++++++ .../app/features/orders/orders-page.jsx | 35 +++++++++++++++ apps/shopify-app/tests/orders-page.test.mjs | 11 ++++- 4 files changed, 126 insertions(+), 5 deletions(-) diff --git a/apps/shopify-app/app/features/orders/order-filters.js b/apps/shopify-app/app/features/orders/order-filters.js index f970172..3da632e 100644 --- a/apps/shopify-app/app/features/orders/order-filters.js +++ b/apps/shopify-app/app/features/orders/order-filters.js @@ -17,6 +17,7 @@ const LEGACY_ORDER_FILTER_QUERY_KEYS = ["q"]; export const ORDER_PLANNING_SCOPE = "planning"; export const ORDER_HISTORY_SCOPE = "history"; export const ORDER_DEFAULT_TAB = "unplanned"; +export const ORDER_DELIVERY_DATE_PENDING = "pending"; export const ORDER_STATUS_TABS = [ { label: "All", value: "all" }, { label: "Unplanned", value: "unplanned" }, @@ -111,10 +112,15 @@ export function orderMatchesFilters(order, filters = {}, options = {}) { return false; } - const deliveryDateFilter = normalizeDateOnlyValue(normalizedFilters.deliveryDate); + const deliveryDateFilter = normalizedFilters.deliveryDate; + const orderDeliveryDate = getOrderDeliveryDateValue(order); + if (deliveryDateFilter === ORDER_DELIVERY_DATE_PENDING && orderDeliveryDate) { + return false; + } if ( deliveryDateFilter && - getOrderDeliveryDateValue(order) !== deliveryDateFilter + deliveryDateFilter !== ORDER_DELIVERY_DATE_PENDING && + orderDeliveryDate !== deliveryDateFilter ) { return false; } @@ -175,6 +181,28 @@ export function getOrderFilterOptions(orders) { }; } +export function getOrderDeliveryDateFilterOptions(orders) { + const counts = new Map(); + + for (const order of Array.isArray(orders) ? orders : []) { + const value = getOrderDeliveryDateValue(order) || ORDER_DELIVERY_DATE_PENDING; + counts.set(value, (counts.get(value) ?? 0) + 1); + } + + return [ + ...(counts.has(ORDER_DELIVERY_DATE_PENDING) + ? [{ + count: counts.get(ORDER_DELIVERY_DATE_PENDING), + value: ORDER_DELIVERY_DATE_PENDING, + }] + : []), + ...Array.from(counts.entries()) + .filter(([value]) => value !== ORDER_DELIVERY_DATE_PENDING) + .sort(([leftValue], [rightValue]) => leftValue.localeCompare(rightValue)) + .map(([value, count]) => ({ count, value })), + ]; +} + export function getOrderFiltersFromSearchParams(searchParams) { const params = toSearchParams(searchParams); const plannedQueryKey = ORDER_FILTER_QUERY_KEYS.planned; @@ -224,7 +252,7 @@ export function normalizeOrderFilters(filters = {}) { return { deliveryArea: textOrEmpty(filters.deliveryArea), - deliveryDate: textOrEmpty(filters.deliveryDate), + deliveryDate: normalizeDeliveryDateFilter(filters.deliveryDate), deliveryState: normalizeDeliveryState(filters.deliveryState), deliveryWeekday: normalizeDeliveryWeekday(filters.deliveryWeekday), orderedDate: "", @@ -567,6 +595,15 @@ function normalizeDeliveryWeekday(value) { : ""; } +function normalizeDeliveryDateFilter(value) { + const normalizedValue = textOrEmpty(value).toLowerCase(); + if (normalizedValue === ORDER_DELIVERY_DATE_PENDING) { + return ORDER_DELIVERY_DATE_PENDING; + } + + return normalizeDateOnlyValue(value); +} + function normalizeDeliveryState(value) { const normalizedValue = normalizeComparableText(value); return ORDER_DELIVERY_STATE_OPTIONS.some((option) => option.value === normalizedValue) diff --git a/apps/shopify-app/app/features/orders/order-filters.test.js b/apps/shopify-app/app/features/orders/order-filters.test.js index 54b3b9b..e7f1012 100644 --- a/apps/shopify-app/app/features/orders/order-filters.test.js +++ b/apps/shopify-app/app/features/orders/order-filters.test.js @@ -4,6 +4,7 @@ import { filterOrders, formatServiceTypeLabel, getBulkOrderSelectionState, + getOrderDeliveryDateFilterOptions, getOrderDeliveryExceptionState, getOrderFilterOptions, getOrderFiltersFromSearchParams, @@ -16,6 +17,7 @@ import { isOrderRouteCreated, isOrderRoutePlanningLocked, isOrderSelectableForCurrentWorkset, + ORDER_DELIVERY_DATE_PENDING, updateOrderFilterSearchParams, } from "./order-filters.js"; @@ -158,6 +160,46 @@ test("filters delivery dates with date-only normalization", () => { ); }); +test("filters orders with no delivery date through the pending sentinel", () => { + assert.deepEqual( + filterOrders( + [ + { id: "dated", deliveryDate: "2026-05-18" }, + { id: "missing" }, + { id: "blank", deliveryDate: " " }, + ], + { deliveryDate: ORDER_DELIVERY_DATE_PENDING, scope: "history" }, + ).map((order) => order.id), + ["missing", "blank"], + ); + + const pendingFilters = getOrderFiltersFromSearchParams( + new URLSearchParams(`deliveryDate=${ORDER_DELIVERY_DATE_PENDING}`), + ); + assert.equal(pendingFilters.deliveryDate, ORDER_DELIVERY_DATE_PENDING); + assert.equal( + updateOrderFilterSearchParams(new URLSearchParams(), pendingFilters).get("deliveryDate"), + ORDER_DELIVERY_DATE_PENDING, + ); +}); + +test("builds counted delivery-date options with pending first", () => { + assert.deepEqual( + getOrderDeliveryDateFilterOptions([ + { deliveryDate: "2026-05-15" }, + { deliveryDate: "2026-05-14T13:00:00.000Z" }, + { deliveryDate: "2026-05-15" }, + {}, + { deliveryDate: "" }, + ]), + [ + { count: 2, value: ORDER_DELIVERY_DATE_PENDING }, + { count: 1, value: "2026-05-14" }, + { count: 2, value: "2026-05-15" }, + ], + ); +}); + test("parses and applies search while removing legacy q query parameters", () => { assert.deepEqual( filterOrders(orders, { scope: "history", search: "city centre" }).map((order) => order.id), diff --git a/apps/shopify-app/app/features/orders/orders-page.jsx b/apps/shopify-app/app/features/orders/orders-page.jsx index 47d82dd..1ecceff 100644 --- a/apps/shopify-app/app/features/orders/orders-page.jsx +++ b/apps/shopify-app/app/features/orders/orders-page.jsx @@ -38,6 +38,7 @@ import { } from "./orders-table-columns"; import { filterOrders, + getOrderDeliveryDateFilterOptions, getOrderFilterOptions, getOrderFiltersFromSearchParams, getOrderDeliveryDateValue, @@ -46,6 +47,7 @@ import { hasActiveOrderFilters, isOrderDeliveryComplete, isOrderRouteCreated, + ORDER_DELIVERY_DATE_PENDING, ORDER_DELIVERY_STATE_OPTIONS, ORDER_HISTORY_SCOPE, ORDER_PLANNING_SCOPE, @@ -1584,6 +1586,20 @@ function formatOrderDateValue(value) { return value ? value.replaceAll("-", ".") : ""; } +function formatDeliveryDateFilterLabel(value, count) { + if (value === ORDER_DELIVERY_DATE_PENDING) return `Date pending (${count})`; + + const date = new Date(`${value}T00:00:00.000Z`); + if (Number.isNaN(date.getTime())) return `${value} (${count})`; + + const weekday = new Intl.DateTimeFormat("en-US", { + timeZone: "UTC", + weekday: "short", + }).format(date); + + return `${weekday} ${value.slice(5, 7)}/${value.slice(8, 10)} (${count})`; +} + function formatOrderDateRangeLabel(startDate, endDate) { if (!startDate && !endDate) return ""; if (!endDate || startDate === endDate) return formatOrderDateValue(startDate); @@ -2429,6 +2445,7 @@ function OrdersPageContent({ loaderData }) { ...effectiveOrderFilters, tab: "all", deliveryArea: "", + deliveryDate: "", deliveryState: "", deliveryWeekday: "", orderedDateFrom: "", @@ -2447,6 +2464,12 @@ function OrdersPageContent({ loaderData }) { deliveryArea: "", referenceDate: orderFilterReferenceDate, })).deliveryAreas, + deliveryDates: getOrderDeliveryDateFilterOptions(filterOrders(orderFilterOptionOrders, { + ...effectiveOrderFilters, + tab: "all", + deliveryDate: "", + referenceDate: orderFilterReferenceDate, + })), deliveryWeekdays: getOrderFilterOptions(filterOrders(orderFilterOptionOrders, { ...effectiveOrderFilters, tab: "all", @@ -4707,6 +4730,18 @@ function OrdersPageContent({ loaderData }) { ) : null} + ({ + label: formatDeliveryDateFilterLabel(value, count), + value, + }))} + value={orderFilters.deliveryDate} + onChange={(filterValue) => handleOrderFilterChange("deliveryDate", filterValue)} + onClear={() => handleClearOrderFilter("deliveryDate")} + /> assert.doesNotMatch(ordersPageSource, /safeOrders\.map\(\(order\) =>\s*\(\s* { +test("Orders page filters table rows by order date, delivery date, delivery day, type, and area", () => { assert.match(ordersPageSource, /import \{ Await, useFetcher, useLoaderData, useNavigate, useRevalidator, useSearchParams \} from "react-router"/); assert.match(ordersPageSource, /import \{[\s\S]*filterOrders[\s\S]*getOrderFilterOptions[\s\S]*getOrderFiltersFromSearchParams[\s\S]*ORDER_HISTORY_SCOPE[\s\S]*ORDER_PLANNING_SCOPE[\s\S]*ORDER_WEEKDAY_OPTIONS[\s\S]*updateOrderFilterSearchParams[\s\S]*\} from "(?:\.\.\/features\/orders|\.)\/order-filters"/); assert.match(ordersPageSource, /const \[searchParams, setSearchParams\] = useSearchParams\(\)/); @@ -1598,6 +1598,7 @@ test("Orders page filters table rows by order date, delivery day, type, and area assert.match(ordersPageSource, /const effectiveOrderFilters = useMemo\([\s\S]*ORDER_HISTORY_SCOPE[\s\S]*: orderFilters,[\s\S]*\[activeOrderFilters, orderFilters\]/); assert.match(ordersPageSource, /const orderFilterOptionOrders = useMemo\(\s*\(\) =>\s*activeOrderFilters\s*\? filterOrders\(displayOrders, \{[\s\S]*?\.\.\.effectiveOrderFilters,[\s\S]*?deliveryArea: "",[\s\S]*?deliveryWeekday: "",[\s\S]*?orderedDateFrom: "",[\s\S]*?orderedDateTo: "",[\s\S]*?serviceType: "",[\s\S]*?referenceDate: orderFilterReferenceDate,[\s\S]*?\}\)\s*: displayOrders,\s*\[activeOrderFilters, displayOrders, effectiveOrderFilters, orderFilterReferenceDate\],\s*\)/); assert.match(ordersPageSource, /deliveryAreas: getOrderFilterOptions\(filterOrders\(orderFilterOptionOrders, \{[\s\S]*?deliveryArea: ""/); + assert.match(ordersPageSource, /deliveryDates: getOrderDeliveryDateFilterOptions\(filterOrders\(orderFilterOptionOrders, \{[\s\S]*?deliveryDate: ""/); assert.match(ordersPageSource, /deliveryWeekdays: getOrderFilterOptions\(filterOrders\(orderFilterOptionOrders, \{[\s\S]*?deliveryWeekday: ""/); assert.match(ordersPageSource, /serviceTypes: getOrderFilterOptions\(filterOrders\(orderFilterOptionOrders, \{[\s\S]*?serviceType: ""/); assert.match(ordersPageSource, /const filteredOrders = useMemo\(\s*\(\) =>\s*activeOrderFilters\s*\? filterOrders\(displayOrders, \{[\s\S]*?\.\.\.effectiveOrderFilters,[\s\S]*?referenceDate: orderFilterReferenceDate,[\s\S]*?\}\)\s*: displayOrders,\s*\[activeOrderFilters, displayOrders, effectiveOrderFilters, orderFilterReferenceDate\],\s*\)/); @@ -1653,6 +1654,13 @@ test("Orders page filters table rows by order date, delivery day, type, and area assert.match(ordersPageSource, /options=\{ORDER_WEEKDAY_OPTIONS\}/); assert.match(ordersPageSource, /handleOrderFilterChange\("deliveryWeekday", filterValue\)/); assert.match(ordersPageSource, /clearLabel="Clear delivery day filter"/); + assert.match(ordersPageSource, /aria-label="Filter orders by delivery date"/); + assert.match(ordersPageSource, /label="Delivery date"/); + assert.match(ordersPageSource, /orderFilterOptions\.deliveryDates\.map\(\(\{ count, value \}\) => \(\{/); + assert.match(ordersPageSource, /formatDeliveryDateFilterLabel\(value, count\)/); + assert.match(ordersPageSource, /value=\{orderFilters\.deliveryDate\}/); + assert.match(ordersPageSource, /handleOrderFilterChange\("deliveryDate", filterValue\)/); + assert.match(ordersPageSource, /clearLabel="Clear delivery date filter"/); assert.match(ordersPageSource, /aria-label="Filter orders by service type"/); assert.match(ordersPageSource, /label="Type"/); assert.match(ordersPageSource, /\{ label: "Delivery", value: "DELIVERY" \}/); @@ -1688,7 +1696,6 @@ test("Orders page filters table rows by order date, delivery day, type, and area assert.doesNotMatch(ordersPageSource, /aria-label="Choose order scope"/); assert.doesNotMatch(ordersPageSource, />Planning Scope<\/option>/); assert.doesNotMatch(ordersPageSource, />History \/ All Orders<\/option>/); - assert.doesNotMatch(ordersPageSource, /aria-label="Filter orders by delivery date"/); assert.doesNotMatch(ordersPageSource, /aria-label="Search orders"/); assert.doesNotMatch(ordersPageSource, /placeholder="Search orders"/); assert.doesNotMatch(ordersPageSource, /type="search"/);