From 5d3045030e039177fd71ab2586660f4a941f397c Mon Sep 17 00:00:00 2001 From: Casey Locker Date: Fri, 14 Aug 2026 19:44:00 -0500 Subject: [PATCH 1/2] fix: sort By Item sponsor groups alphabetically The sponsor accordions in the Purchase Details "By Item" report were ordered by total units descending, with the name only as a tiebreak, which reads as no order at all. Nothing sorts that level explicitly (the column headers reorder items within a group), so it now defaults to alphabetical, following the convention already used by the pivot tree: unknown bucket last, then name ascending, case insensitive. This also decides which sponsors land on which client side page. Co-Authored-By: Claude --- src/components/sponsors/reports/ByItemView.js | 16 +++++++++----- .../reports/__tests__/ByItemView.test.js | 22 ++++++++++++++++--- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/components/sponsors/reports/ByItemView.js b/src/components/sponsors/reports/ByItemView.js index dce029a0a..9bb7ce296 100644 --- a/src/components/sponsors/reports/ByItemView.js +++ b/src/components/sponsors/reports/ByItemView.js @@ -177,10 +177,16 @@ export const groupLinesBySponsorItem = (rows = []) => { purchasedCount: items.filter((it) => it.qty > 0).length }; }); - groups.sort( - (a, b) => - b.totalQty - a.totalQty || a.sponsorName.localeCompare(b.sponsorName) - ); + // Nothing sorts the sponsor accordions explicitly (the column headers reorder + // items WITHIN a group), so they default to alphabetical — same convention as + // the pivot tree: unknown bucket last, then label asc case-insensitive. This + // also decides which sponsors land on which client-side page. + groups.sort((a, b) => { + if (!a.sponsorName !== !b.sponsorName) return a.sponsorName ? -1 : 1; + return a.sponsorName + .toLowerCase() + .localeCompare(b.sponsorName.toLowerCase()); + }); return groups; }; @@ -639,7 +645,7 @@ const ByItemView = ({ > itemKey(group, item)} expandedItems={expandedItems} diff --git a/src/components/sponsors/reports/__tests__/ByItemView.test.js b/src/components/sponsors/reports/__tests__/ByItemView.test.js index 46aafdaf6..666df5c5b 100644 --- a/src/components/sponsors/reports/__tests__/ByItemView.test.js +++ b/src/components/sponsors/reports/__tests__/ByItemView.test.js @@ -106,7 +106,7 @@ describe("groupLinesBySponsorItem", () => { expect(group.items.find((i) => i.itemCode === "B").totalCents).toBe(100); }); - it("sorts items qty desc then orders desc, sponsors by totalQty desc", () => { + it("sorts items qty desc then orders desc", () => { const rows = [ line({ sponsor: { id: 1, name: "Small" }, item_code: "A", quantity: 1 }), line({ @@ -124,9 +124,25 @@ describe("groupLinesBySponsorItem", () => { }) ]; const groups = groupLinesBySponsorItem(rows); - expect(groups.map((g) => g.sponsorName)).toEqual(["Big", "Small"]); + const big = groups.find((g) => g.sponsorName === "Big"); // C: qty 9, orders 2 beats B: qty 9, orders 1 - expect(groups[0].items.map((i) => i.itemCode)).toEqual(["C", "B"]); + expect(big.items.map((i) => i.itemCode)).toEqual(["C", "B"]); + }); + + it("sorts sponsor groups alphabetically, not by qty, with unknown last", () => { + const rows = [ + line({ sponsor: { id: 1, name: "zeta" }, quantity: 1 }), + line({ sponsor: null, quantity: 1 }), + // Biggest qty by far — it still sorts on name, not units. + line({ sponsor: { id: 2, name: "Nokia" }, quantity: 99 }), + line({ sponsor: { id: 3, name: "acme" }, quantity: 1 }) + ]; + expect(groupLinesBySponsorItem(rows).map((g) => g.sponsorName)).toEqual([ + "acme", + "Nokia", + "zeta", + "" + ]); }); it("passes canceled lines through as contributors with isCanceled", () => { From 7ef25ae7f62cf1a98e44e45e713d295330c6b8e7 Mon Sep 17 00:00:00 2001 From: Casey Locker Date: Fri, 14 Aug 2026 19:49:24 -0500 Subject: [PATCH 2/2] docs: explain why sponsor unknown-last keys on name, not id The previous comment claimed the comparator followed the pivot tree convention, but the pivot tree keys unknown on the sponsor id (pivot-defs.js AXES.sponsor.isUnknown) while this one keys on the name. That divergence is deliberate, so say why instead of misdescribing it. Comment only, no behavior change. Co-Authored-By: Claude --- src/components/sponsors/reports/ByItemView.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/sponsors/reports/ByItemView.js b/src/components/sponsors/reports/ByItemView.js index 9bb7ce296..1f644ef72 100644 --- a/src/components/sponsors/reports/ByItemView.js +++ b/src/components/sponsors/reports/ByItemView.js @@ -178,9 +178,13 @@ export const groupLinesBySponsorItem = (rows = []) => { }; }); // Nothing sorts the sponsor accordions explicitly (the column headers reorder - // items WITHIN a group), so they default to alphabetical — same convention as - // the pivot tree: unknown bucket last, then label asc case-insensitive. This - // also decides which sponsors land on which client-side page. + // items WITHIN a group), so they default to alphabetical, unknown bucket last. + // That test keys on the NAME, not sponsorId, because the card title renders as + // `sponsorName || "Unknown sponsor"` — sorting on what the card displays keeps + // every "Unknown sponsor" card together at the end. (The pivot tree keys on + // the id instead, which splits them: a blank-named sponsor shows the unknown + // label but sorts first.) This also decides which sponsors land on which + // client-side page. groups.sort((a, b) => { if (!a.sponsorName !== !b.sponsorName) return a.sponsorName ? -1 : 1; return a.sponsorName