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
43 changes: 40 additions & 3 deletions apps/shopify-app/app/features/orders/order-filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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: "",
Expand Down Expand Up @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions apps/shopify-app/app/features/orders/order-filters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
filterOrders,
formatServiceTypeLabel,
getBulkOrderSelectionState,
getOrderDeliveryDateFilterOptions,
getOrderDeliveryExceptionState,
getOrderFilterOptions,
getOrderFiltersFromSearchParams,
Expand All @@ -16,6 +17,7 @@ import {
isOrderRouteCreated,
isOrderRoutePlanningLocked,
isOrderSelectableForCurrentWorkset,
ORDER_DELIVERY_DATE_PENDING,
updateOrderFilterSearchParams,
} from "./order-filters.js";

Expand Down Expand Up @@ -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),
Expand Down
35 changes: 35 additions & 0 deletions apps/shopify-app/app/features/orders/orders-page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
} from "./orders-table-columns";
import {
filterOrders,
getOrderDeliveryDateFilterOptions,
getOrderFilterOptions,
getOrderFiltersFromSearchParams,
getOrderDeliveryDateValue,
Expand All @@ -46,6 +47,7 @@ import {
hasActiveOrderFilters,
isOrderDeliveryComplete,
isOrderRouteCreated,
ORDER_DELIVERY_DATE_PENDING,
ORDER_DELIVERY_STATE_OPTIONS,
ORDER_HISTORY_SCOPE,
ORDER_PLANNING_SCOPE,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -2429,6 +2445,7 @@ function OrdersPageContent({ loaderData }) {
...effectiveOrderFilters,
tab: "all",
deliveryArea: "",
deliveryDate: "",
deliveryState: "",
deliveryWeekday: "",
orderedDateFrom: "",
Expand All @@ -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",
Expand Down Expand Up @@ -4707,6 +4730,18 @@ function OrdersPageContent({ loaderData }) {
)
: null}
</div>
<OrderFilterMenu
aria-label="Filter orders by delivery date"
clearLabel="Clear delivery date filter"
label="Delivery date"
options={orderFilterOptions.deliveryDates.map(({ count, value }) => ({
label: formatDeliveryDateFilterLabel(value, count),
value,
}))}
value={orderFilters.deliveryDate}
onChange={(filterValue) => handleOrderFilterChange("deliveryDate", filterValue)}
onClear={() => handleClearOrderFilter("deliveryDate")}
/>
<OrderFilterMenu
aria-label="Filter orders by delivery day"
clearLabel="Clear delivery day filter"
Expand Down
11 changes: 9 additions & 2 deletions apps/shopify-app/tests/orders-page.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1585,7 +1585,7 @@ test("Orders table headers sort rows by ascending and descending values", () =>
assert.doesNotMatch(ordersPageSource, /safeOrders\.map\(\(order\) =>\s*\(\s*<tr/);
});

test("Orders page filters table rows by order date, delivery day, type, and area", () => {
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\(\)/);
Expand All @@ -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*\)/);
Expand Down Expand Up @@ -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" \}/);
Expand Down Expand Up @@ -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"/);
Expand Down