Merge OS summit admin in - #4
Merged
Merged
Conversation
Signed-off-by: Sebastian Marcet <smarcet@gmail.com>
Signed-off-by: Sebastian Marcet <smarcet@gmail.com>
Signed-off-by: Sebastian Marcet <smarcet@gmail.com>
caseylocker
added a commit
that referenced
this pull request
Jul 3, 2026
…ore dedup, paging, test prune) Reducer-driven list state (#1): - Move pagination/sort/filter out of component useState into the purchase-details, lines, and sponsor-asset reducers; thunks record them via getRequest's 5th-arg REQUEST payload and components read from props. Mirrors ticket-type-list-page. Adopt uicore components, drop duplicates (#2-#4 + parent sweep): - MuiDropdown for the status/form/payment selects, sponsor-asset status, and PivotSelector; uicore SearchInput in FilterBar; uicore DateTimePicker for the date filters (values kept as YYYY-MM-DD via a UTC boundary conversion, query contract unchanged). - SummaryPanel -> uicore InlineCard; StatusRollupChips -> mui/chip-list. Use the global loading overlay (#5): - Remove per-slice `loading` from all four reports reducers; thunks bracket start/stopLoading so state.baseState.loading + the single AjaxLoader own it. Sponsor-asset keeps a local hasFetched flag for first-mount empty-state suppression. Fold single-consumer helper (#6): - Inline bucketLinesBySponsor into LinesManifestView; delete manifest-grouping.js. Bulk-load sponsor-asset rows with bounded concurrency (#10 / smarcet): - Replace the per_page=5000 fetch (removed ALL_ROWS_PER_PAGE) with page-1-then-parallel paging at per_page=100 via p-limit + Promise.all, matching ticket-actions. Re-check the sequence token inside the pool callback so a superseded invocation's queued jobs cannot fire an identical-page request that would abort the fresh invocation's (under getRequest's page-bearing abort key). Follow-up correctness fixes (Codex review): - toReportDate treats the picker's moment(0) clear-sentinel as "no date" (matches the .unix()||null filter convention) so clearing a date drops the filter instead of sending 1970-01-01. - FilterBar search applies live (debounced) instead of feeding the Apply-gated draft, so a fast type+Apply can't submit a stale value. - Generation-guard hasFetched so a superseded fetch can't flash the empty state. Prune tests to meaningful coverage (parent + #7-#9): - Drop tests that exercised third-party libraries or were low-value (summit-switch, ReportShell, reducer-loading, TierBadge neutral, the manifest-grouping unit test) and reduce sponsor-reports suites to the behavior we own. Add mutation-proof regression tests for the fixes above. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF
caseylocker
added a commit
that referenced
this pull request
Jul 3, 2026
…ore dedup, paging, test prune) Reducer-driven list state (#1): - Move pagination/sort/filter out of component useState into the purchase-details, lines, and sponsor-asset reducers; thunks record them via getRequest's 5th-arg REQUEST payload and components read from props. Mirrors ticket-type-list-page. Adopt uicore components, drop duplicates (#2-#4 + parent sweep): - MuiDropdown for the status/form/payment selects, sponsor-asset status, and PivotSelector; uicore SearchInput in FilterBar; uicore DateTimePicker for the date filters (values kept as YYYY-MM-DD via a UTC boundary conversion, query contract unchanged). - SummaryPanel -> uicore InlineCard; StatusRollupChips -> mui/chip-list. Use the global loading overlay (#5): - Remove per-slice `loading` from all four reports reducers; thunks bracket start/stopLoading so state.baseState.loading + the single AjaxLoader own it. Sponsor-asset keeps a local hasFetched flag for first-mount empty-state suppression. Fold single-consumer helper (#6): - Inline bucketLinesBySponsor into LinesManifestView; delete manifest-grouping.js. Bulk-load sponsor-asset rows with bounded concurrency (#10 / smarcet): - Replace the per_page=5000 fetch (removed ALL_ROWS_PER_PAGE) with page-1-then-parallel paging at per_page=100 via p-limit + Promise.all, matching ticket-actions. Re-check the sequence token inside the pool callback so a superseded invocation's queued jobs cannot fire an identical-page request that would abort the fresh invocation's (under getRequest's page-bearing abort key). Follow-up correctness fixes (Codex review): - toReportDate treats the picker's moment(0) clear-sentinel as "no date" (matches the .unix()||null filter convention) so clearing a date drops the filter instead of sending 1970-01-01. - FilterBar search applies live (debounced) instead of feeding the Apply-gated draft, so a fast type+Apply can't submit a stale value. - Generation-guard hasFetched so a superseded fetch can't flash the empty state. Prune tests to meaningful coverage (parent + #7-#9): - Drop tests that exercised third-party libraries or were low-value (summit-switch, ReportShell, reducer-loading, TierBadge neutral, the manifest-grouping unit test) and reduce sponsor-reports suites to the behavior we own. Add mutation-proof regression tests for the fixes above. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF
smarcet
pushed a commit
that referenced
this pull request
Jul 6, 2026
…nsors → Reports) (#997) * feat(sponsor-reports): scaffold admin-gated Reports section under Sponsors Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): port actions/reducers/utils + store wiring Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): port shared report components (T.translate, getCSV) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): Purchase Details report (MuiTable, conditional refund tile) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(sponsor-reports): cover CSV+pagination/sort dispatch; drop vestigial SORT_FIELD_MAP Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): Sponsor Assets report + drill-down (rewired routes) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): Reports landing + integration Replace placeholder with two MUI card links (Purchase Details, Sponsor Assets); add breadcrumb; add landing_title i18n key; update layout test for card assertions; 6 new tests, 912 total. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(sponsor-reports): final-review cleanup — drilldown errors, i18n, prop, route test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): drop vestigial export-disabled; route to READ_ERROR Remove *_EXPORT_DISABLED action constants (purchase-details, sponsor-asset, drilldown) and their dead reducer arms. Route onExportDisabled → READ_ERROR in getPurchaseDetailsReport and getSponsorAssetReport thunks (mirrors the already-fixed drilldown), so a 503 export-disabled on a read path clears loading via the same arm as not-found/unauthorized. Remove exportDisabled and exportError from all three reducer DEFAULT_STATEs, drop disabled={exportDisabled} from all three ExportCsvButton usages, and clean up all test references to the deleted constants and unreachable state. Add 503 export-disabled coverage for getPurchaseDetailsReport and getSponsorAssetReport matching the existing drilldown tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): UI polish (fonts, breadcrumb trail, asset spacing) - SummaryPanel tile labels overline -> subtitle1 (bigger; both reports) - GroupByToggle small -> medium + larger button text - SponsorReportsLayout renders a persistent Reports crumb + per-route page crumb so the trail reads .../Sponsors/Reports/<page> (was dropping to .../Sponsors on sub-pages); landing's own crumb removed (no dup) - Sponsor Assets: gap between Report Filters and summary cards - tests updated (landing breadcrumb moved to layout; layout trail test added) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): address CodeRabbit review (PR #997) - guard all report thunks against a missing currentSummit (prevents a stuck spinner from base(currentSummit.id) throwing after startLoading) - formatUsd: treat blank/whitespace strings + non-finite numbers as missing (em dash) instead of $0.00; add reports-money tests - FilterBar: re-sync draft when the committed value prop changes externally - drop redundant routing.test.js (real-layout integration test in sponsor-reports-layout.test.js already covers route ordering) - add PURCHASE_DETAILS_VALIDATION_CLEAR reducer test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): address Copilot review (PR #997) - reports-text: decode uppercase/mixed-case HTML entities (the case-insensitive match was missing the lowercase ENTITIES map); add toPlainText tests - report-errors: export-disabled now dispatches { kind, status, message } to match the other error branches - report-query: coerce sponsorIds to positive integers and drop the rest so the filter never emits sponsor_id==NaN/==0; add buildReportQuery tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(sponsor-reports): request dedicated sponsor-reports scopes (review) Per Sebastian's PR review: summit-admin now requests the new microservice's own scopes (sponsor-reports/read, sponsor-reports/export) instead of relying on the broad reports/all. Adds SPONSOR_REPORTS_SCOPES and wires it into SCOPES (reports/all kept for the legacy summit-reports-api). Requires the IDP to grant these two scopes to the summit-admin client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add purchase-details lines report action Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add purchase-details lines reducer + store registration Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add bucketLinesBySponsor grouping util Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add LinesManifestView collapsible per-line manifest Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add Orders/Line-Items report view toggle Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): wire Line Items manifest view into Purchase Details page Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(sponsor-reports): strip trailing whitespace in LinesManifestView Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): address PR #997 convention review (smarcet) - index.js: import DEFAULT_PER_PAGE from constants (drop local DEFAULT_PAGE_SIZE dup) - index.js: mapDispatchToProps to object shorthand via new clearPurchaseDetailsValidation action creator (removes the raw dispatch prop) - OrdersTable.js: remove dead getOrderRowId export + its test (page maps id inline) - usePrint: move to src/hooks/ per the codebase custom-hook convention - section-csv-query.js: align defense-in-depth comment with the sponsor_id/page_id guard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(sponsor-reports): add lines slice to layout test store (fix CI) The Purchase Details page mapStateToProps reads sponsorReportsPurchaseDetailsLinesState.data; the SponsorReportsLayout test mounts the real page, so its mock store must include that slice. Missing it threw 'Cannot read properties of undefined (reading data)' under the full yarn test run (CI), though the scoped report-suite runs passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): wrap overflowing asset filenames in drilldown links Hashed asset filenames (32-char hex prefix, no spaces) overflowed their card. The link Typography had noWrap (white-space:nowrap). Switch the link to a flex row with overflowWrap:anywhere + minWidth:0 on the filename so the unbroken hash wraps within the card; icons get flexShrink:0 so they don't squash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): readable toggle + summary-tile typography The app sets html root font-size to 62.5% (10px), so rem-based sizes render tiny: the toggle override read 8.75px and subtitle1 tile labels 10px. Pin px instead — toggles to 14px/500 to match the Print/Export action buttons, and the summary-tile labels to 14px. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): visible sponsor avatars + white header-icon theme Add SponsorAvatar: logo-first with a colored-initials fallback so a no-logo (or white-on-white) sponsor is never an invisible blank. Styled to mimic the ReportShell title icon — single primary.light rounded square with white foreground — and used in both group-by views and the drill-down header. Switch the ReportShell title icon glyph to white to match. Also wrap the component-view asset filename (was truncated by noWrap+maxWidth) so the full name is readable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): enable CSV export on the Line Items view Render the Export CSV button on the Line Items (per-line) view of the Purchase Details report, wired to the new backend GET .../purchase-details/lines/csv endpoint. One ExportCsvButton switched by view; lines query strips only pagination (preserves filters incl. the derived include_cancelled). Orders export unchanged. Label stays 'Export CSV'; filename purchase-details-lines-summit-<id>.csv. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): add isPositiveIntId + htmlToPlainText to methods.js Generic helpers move to the shared methods.js home (convention). htmlToPlainText preserves tag-boundary whitespace + decodes entities (htmlToString fuses words). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): co-locate report-query + add shared purchase query builders Move report-query under the feature folder (feature helper convention). Add buildPurchaseQuery/buildPurchaseLinesQuery so the on-screen fetch and the upcoming export thunks share one date/order normalization (no duplication, no regression). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): use methods.htmlToPlainText, drop reports-text util Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): purchase-details CSV export via action thunks Move orders/lines CSV URL+params+filename building into exportPurchaseDetailsCsv/ exportPurchaseDetailsLinesCsv (cf. exportEventRsvpsCSV); page dispatches them from a plain MUI Button. Orders CSV keeps the on-screen sort. base() now reads window.SPONSOR_REPORTS_API_URL directly (drops getReportsApiBaseUrl import). currentSummit removed from component props/mapStateToProps (no longer needed after CSV URL building moved to the thunks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): sponsor-asset CSV export via action thunks; drop ExportCsvButton Add exportSponsorAssetCsv/exportSponsorAssetSectionCsv (section logic folded in, simplified); pages dispatch from plain buttons. Remove the ported ExportCsvButton and section-csv-query util. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): delete reports-api util; wire isPositiveIntId from methods - sponsor-asset-report-page/index.js: repoint isPositiveIntId import to utils/methods - sponsor-asset-drilldown-page/index.js: merge htmlToPlainText + isPositiveIntId into one import from utils/methods (fixes import/no-duplicates) - sponsor-asset-report-page/__tests__: remove jest.mock("reports-api") — pure fn, no mock needed - sponsor-asset-drilldown-page/__tests__: same - layouts/__tests__/sponsor-reports-layout.test.js: remove obsolete reports-api mock + comment - purchase-details-report-page/__tests__: rename stale "ExportCsvButton" test label to "export button" (authorized micro-cleanup; component deleted in Task 5) - git rm src/utils/reports-api.js — no longer consumed by any source or test - 115 suites / 966 tests passing; 0 new lint errors Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): co-locate report-errors + reports-money out of shared utils Both files moved into src/pages/sponsors/sponsor-reports/ per §2 (feature helpers never in shared utils/); test for reports-money co-located alongside. Internal ./constants import in report-errors updated to ../../../utils/constants. formatUsd logic unchanged — uicore/cents contract deferred to PR-B. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): update constants import path in moved report-errors.js ./constants was not re-staged after the path update; this corrects it to ../../../utils/constants (resolves from the new feature folder location). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reports): replace formatUsd with uicore currencyAmountFromCents Backend now emits money fields as integer cents. Drop the bespoke reports-money.js formatter (per santi PR #997 review) and use the platform-wide currencyAmountFromCents from openstack-uicore-foundation. Null guard via inline/local const rather than a new shared helper file. Test fixtures updated to cents input; assertions to no-comma "$X.XX" output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reports): use cents fixtures in purchase-details reducer test Codex follow-up: the reducer passes summary payload through unchanged, so the dollar-string fixtures were not a live bug, but they're stale under the cents contract. Switch total_paid fixtures to integer cents to stay representative. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reports): dedup per-page/ms constants via src/utils/constants - OrdersTable: replace local MS_PER_SECOND (1000) with MILLISECONDS_IN_SECOND and local DEFAULT_PER_PAGE (10) with the shared constant; remove eslint-disable - LinesManifestView: replace magic-number PER_PAGE_OPTIONS [10,25,50,100] and DEFAULT_PER_PAGE (50) with named constants [DEFAULT_PER_PAGE, TWENTY_PER_PAGE, FIFTY_PER_PAGE, MAX_PER_PAGE]; default perPage prop is now FIFTY_PER_PAGE Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * fix(sponsor-reports): return Promise.resolve() on all thunk guard branches Every thunk in sponsor-reports-actions.js that guards against a missing summit returned `undefined` synchronously while the normal code-path returned a Promise — inconsistent for callers using `.then()`/`await`. Changed all ten guard-branch returns to `return Promise.resolve()` so every thunk is uniformly thenable regardless of which branch is taken. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): fold statusTone into StatusPill Moves TONE_BY_STATUS + statusTone() from the standalone statusTone.js into StatusPill.js as a named export. Updates StatusRollupChips.js to import from StatusPill. Deletes statusTone.js and its test file; all branch-coverage assertions migrated into StatusPill.test.js. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(reports): replace hand-rolled formatCheckoutTime with moment Swaps the manual regex + AM/PM math in formatCheckoutTime for moment.unix().utc() / moment.utc() + .format("YYYY-MM-DD h:mm A"). Output is byte-identical for all tested inputs. Removes dead NOON constant and unused MILLISECONDS_IN_SECOND import; keeps ISO_DATE_LENGTH (still needed for the date-only fallback path, and the no-magic-numbers rule requires it named). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): adopt useSnackbarMessage hook for 412 validation toast Replaces the hand-rolled inline MUI <Snackbar>/<Alert> with the existing useSnackbarMessage hook (openstack-uicore-foundation), consistent with payment-view.js and mui-qr-badge-popup.js. Removes TOAST_AUTO_HIDE_MS and the Snackbar import; Alert import kept (still used for readError). Redux clearPurchaseDetailsValidation lifecycle preserved via useEffect. Tests: mocks useSnackbarMessage, asserts errorMessage is called with the correct message (and i18n fallback), clearValidation is dispatched, and errorMessage is not called when validationError is null. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * test(sponsor-reports): mock useSnackbarMessage in layout test The Purchase Details page now calls useSnackbarMessage() (T5); the layout integration test renders that page without the global snackbar provider, so the hook destructured undefined and threw on render. Mock it like the page's own test does. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(T6): move manifest-grouping into reports feature dir bucketLinesBySponsor had exactly one production caller (LinesManifestView). Moving the helper and its test out of src/utils/ and co-locating them with the feature removes the one-off-file-in-shared-utils anti-pattern flagged in PR review. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): T7 — dedup page-size constant + fix default-param-last Edit A: replace local LINES_DEFAULT_PAGE_SIZE = 50 with imported FIFTY_PER_PAGE from utils/constants; Edit B: remove `filters = {}` default on exportPurchaseDetailsCsv — all callers pass an explicit object, buildPurchaseQuery already defaults its own first arg. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): convert nextDayStartIso to moment-timezone Replace hand-rolled new Date / setUTCDate with moment.utc(ymd, "YYYY-MM-DD", true).add(1, "day") for consistency with the rest of the date-formatting in this feature. Output is byte-identical; ISO_DATE_LENGTH removed (now unused). Invalid-input guard returns ymd unchanged rather than throwing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * test(OrdersTable): pin moment.utc offset+malformed contract (T9) Add tests for non-Z offset inputs (-05:00, +05:00) and malformed ISO- like strings to lock current moment.utc() behavior. Backend emits UTC Z only; offset path is inert in production — assertions are documentation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): use repo-native isImageUrl + DEFAULT_CURRENT_PAGE Replace local IMAGE_EXT regex in sponsor-asset-drilldown-page with the repo-canonical isImageUrl from utils/methods (same extensions, also handles query strings). Replace local FIRST_PAGE = 1 in sponsor-asset-report-page with DEFAULT_CURRENT_PAGE from utils/constants (7 references). Replace bare useState(1) / setPage(1) page-init/reset literals in purchase-details- report-page with DEFAULT_CURRENT_PAGE (9 references); orderDir useState(1) (sort direction) is intentionally unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): translate OrdersTable column headers via i18n Replace 7 bare-string header values with T.translate calls; add 5 new col_* keys to en.json; update header assertions in both test files to match key output from the translate mock. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): use DEFAULT_CURRENT_PAGE for page-number defaults Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): use Grid2 in asset drilldown page Replace deprecated MUI v4 Grid (item xs={}) with Grid2 size={{}} API, matching every other file in this PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(sponsor-reports): use object-shorthand mapDispatchToProps in asset pages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): move query builders to actions layer, fold in toOrderParam Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): build report queries inside thunks, not components Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): move report-errors helper to actions layer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * docs(sponsor-reports): fix stale toOrderParam comment in OrdersTable toOrderParam moved to the query thunk in an earlier commit; the column comment no longer describes a page-handler conversion. * feat(sponsor-reports): add ContentTypeToggle (Collected/All) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * feat(sponsor-reports): Collected/All content filter on asset report page Wire ContentTypeToggle (default: collected) into SponsorAssetReportPage — injects moduleType:"Media" on fetch + CSV export when collected, omits it for all; resets page on toggle. Tests: 12/12 (+3 new), full suite 995/995. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * feat(sponsor-reports): suppress download status + Collected/All filter in drilldown Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * fix(sponsor-reports): show empty page sections under All in drilldown Drop-empty-section now applies only in Collected mode; All renders every section as-is, including a page the sponsor never submitted to. * refactor(sponsor-reports): hard-wire sponsor assets to collected (Media); remove toggle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * fix(sponsor-reports): scope section CSV to collected (Media); restore HTML-flatten test - exportSponsorAssetSectionCsv now filters module_type==Media so the per-page Download CSV matches the collected-only view (was an escape path for Document/Info rows). - Restore ContentCell HTML-flatten coverage as a Media text/input row (the prior test used an Info row removed with the toggle teardown). * test(sponsor-reports): strengthen ContentCell flatten test to pin htmlToPlainText Use an input with entities + adjacent tags (<p>Booth A</p><p>B & C</p> → "Booth A B & C") so the test exercises entity-decode + whitespace-collapse + tag-to-space — the behavior that distinguishes htmlToPlainText from stripTags. A regression swapping the helper would now fail. * refactor(sponsor-reports): reuse isPositiveIntId + TWENTY_PER_PAGE; drop port-framing comments Convention-sweep fixes (Codex full-branch pass): - query + section-CSV thunks reuse isPositiveIntId instead of hand-rolled Number.isInteger ID checks; section CSV bails on an invalid id rather than emitting a broadened (whole-sponsor) export - replace local GROUP_PER_PAGE=25 (an arbitrary literal carried from the sponsor-services source, not spec'd) with the existing TWENTY_PER_PAGE - rewrite OrdersGrid/sponsor-services port-framing comments to describe summit-admin behavior only Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): replace report-print.css with MUI GlobalStyles Removes the last standalone stylesheet in the reports feature (legacy in summit-admin per santipalenque). Whole-page print isolation is preserved verbatim via an inline <GlobalStyles> block in ReportShell; the .report-body className anchor is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * refactor(sponsor-reports): extract ContentCell into a shared component Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add pivot axis + named-pivot definitions Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): pure recursive pivot-tree grouping + usePivot memo Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): fetch full flat asset set atomically with last_page paging Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add PivotSelector Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add recursive PivotTree renderer Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): multi-level pivot view replaces single-level grouping Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(sponsor-reports): cover getSponsorAssetRows multi-page paging loop Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): remove orphaned grouped-fetch path + dead exports; fix empty-state flash Retire getSponsorAssetReport (no callers after the pivot rewire), its dead reducer fields/case and the group_by query branch, drop unused axisKeyOf/axisLabelOf, and default the reducer loading flag true to avoid a no-results flash before first fetch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): drop stale getSponsorAssetRows completions (request token) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * feat(sponsor-reports): route pivot + unknown-bucket labels through i18n Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): trim component pivot label (key/label parity) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): close both stale-dispatch holes in getSponsorAssetRows via guardedDispatch Introduces a single `guardedDispatch` closure (checked against `mySeq` at every call site) to replace the two existing ad-hoc terminal guards. Hole 1 — stuck loading: REQUEST_SPONSOR_ASSET was dispatched after `await getAccessTokenSafely()` without a seq-guard. A stale call that resumed after a newer call had already committed RECEIVE_SPONSOR_ASSET_ROWS would re-flip loading:true with no terminal to clear it. guardedDispatch suppresses the REQUEST dispatch for any superseded invocation. Hole 2 — persistent stale error: fetchPage passed raw dispatch to getRequest, so a stale request's HTTP error handler fired SPONSOR_ASSET_READ_ERROR unguarded — clobbering fresh success state with readError. Changing the innermost `)(dispatch)` to `)(guardedDispatch)` threads the guard through getRequest's error-handler invocation. The RECEIVE and catch-block SPONSOR_ASSET_READ_ERROR terminals now also use guardedDispatch, consolidating all four dispatch sites under one mechanism. Tests added (getSponsorAssetRows): • suppresses REQUEST_SPONSOR_ASSET from a stale call (prevents stuck-loading) • suppresses SPONSOR_ASSET_READ_ERROR from a stale call's error handler Full suite: 1089/1089 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): drop the always-zero N/A status bucket from Sponsor Assets UI The report is scoped to Media assets (server applies module_type==Media), whose completion strategies only ever emit pending/in_progress/completed — never not_applicable. The N/A summary tile and per-group rollup chip were therefore always zero. Remove not_applicable from the tile keys, rollup chips, pivot-tree accumulator, and the now-dead i18n key. Backend contract is untouched (it may still send the key; the FE simply ignores it). StatusPill's generic not_applicable->default tone mapping is left as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add Status filter to Sponsor Assets report (JP request) Single-select Status filter (Any / Completed / In Progress / Pending) in the Sponsor Assets report's FilterBar extraControls slot. Pure FE: the backend SponsorAssetFilter already allows status==, buildReportQuery already emits it, and every row carries status — only the UI control was missing. Reuses STATUS_TILE_KEYS so the options stay in lockstep with the summary tiles. Applies server-side (refetch on Apply), so the summary tiles recompute on the filtered set and CSV export inherits the filter. No backend change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): map Payment Method filter to payment_method== query bracket * feat(sponsor-reports): add payment method + invoice detail columns to Purchase Details orders table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * feat(sponsor-reports): add Payment Method filter to Purchase Details report Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(sponsor-reports): tighten finance-column assertions (all 4 non-sortable, exact em-dash count) * refactor(sponsor-reports): rework read-error handling + address PR #997 review Addresses the smarcet + santipalenque CHANGES_REQUESTED on the Sponsor Reports FE port, plus a whole-PR convention sweep and correctness pass (each verified with an independent review). Error handling: - Delete sponsor-reports-errors.js; fold a module-private reportReadErrorHandler into sponsor-reports-actions.js, matching the per-feature handler precedent in sponsor-cart-actions.js / summit-actions.js (src/actions holds only action files). - Delegate 401 to uicore authErrorHandler so its isClearingSessionState guard dedupes re-login across the paginated getSponsorAssetRows fetch (was calling doLogin() raw per page). - Drop the dead classifyReportError taxonomy (onExportDisabled was wired to the same action as onReadError everywhere; the kind zoo was unused). Keep only what pages consume: inline readError.message, kind:"not-found" for the drilldown 404, and the Purchase Details 412 validation toast. Reports keep inline error bodies (not the app snackbar) by design. - Gate the getSponsorAssetRows catch to genuine non-HTTP exceptions (e instanceof Error): uicore rejects HTTP failures with a plain {err,res,...} object after invoking the handler, so the old unconditional catch flashed an inline error over the 401 reauth and clobbered the handler's server message on 403/500. Correctness: - Escape the five string filter[] operands (status, form_code, payment_method, module_type, media_request_type) with escapeFilterValue, per the FN filtering convention (cf. sponsor-cart-actions.js / event-rsvp-actions.js). Numeric ids, ISO dates, and the top-level search param stay literal. - getSponsorAssetRows: bail a superseded invocation before it fires a request (after the token await and in the pagination loop). uicore getRequest aborts the in-flight same-URL request, so a stale call could otherwise cancel the current load. Complements the existing seq-guard on dispatches. - Drilldown empty-state: guard on visiblePages.length (Media-filtered) instead of pages.length, so a sponsor whose pages hold only non-Media modules shows the no-submissions panel rather than an empty body. Convention sweep: - Fold sponsor-reports-query.js into sponsor-reports-actions.js (only non-*-actions file in src/actions, imported only there); builders stay exported for their tests. - Move usePivot to src/hooks (canonical hook location, cf. usePrint). - Remove 6 dead i18n keys under sponsor_reports_page (live pivot_unnamed_component untouched). - Optional-chaining sweep for row.module to match PivotTree's defensive idiom: ContentCell (2) + sponsor-asset-drilldown-page (4). Rewrites the handler's tests as thunk-level integration tests; adds live-401 / live-403 asset-fetch and no-submissions coverage. Full suite green (131 suites / 1130 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): pattern-audit follow-ups (filter scope, 404 route, hook fold) Three issues surfaced by a whole-PR pattern audit against summit-admin exemplars. - Payment Method filter silently no-op'd on the Line Items view. The Purchase Details page shares one filters object across the orders/lines toggle, but the lines endpoint's filter set omits payment_method (order-level attribute) and BaseFilter silently skips unknown AND-filter fields — so selecting a payment method returned unfiltered lines. Gate the control to the orders view (mirrors search being view-specific) and strip paymentMethod in buildPurchaseLinesQuery so a value carried over from the orders view can't emit a dead clause. Orders view is unchanged (buildPurchaseQuery still emits payment_method). - Reports layout had no catch-all route, so an unknown /reports/... path rendered only the breadcrumb shell. Add <Route component={NoMatchPage} /> last in the Switch, matching sponsor-layout/event-layout. Eager import (not lazy) to match this layout's own convention — it eager-imports its pages and has no Suspense. - Fold usePivot into its only consumer (sponsor-asset-report-page) and delete the hook. A single-export helper with one consumer belongs in the consumer, not the shared src/hooks dir (react-frontend.md §2). usePrint stays in src/hooks — it's generic (window.print) and used by all three report pages. Full suite green (131 suites / 1131 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * test(sponsor-reports): regression tests for the summit-gate rebuttal Encodes the invariant behind the "mount-only effect never re-runs on summit change → stuck empty" review concern, so a future refactor that breaks it fails CI instead of silently reintroducing the bug. - sponsor-reports-actions: mount fetches (getSponsorAssetFilters/Rows, getPurchaseDetailsFilters) no-op with no current summit, across null / {} / { id: 0 } (a weaker `!currentSummit` guard fails the {} and { id: 0 } cases). - summit-id-layout: the gate renders nothing unless currentSummit.id && summitId === currentSummit.id && !loading && hasLoaded, with each negative case isolating ONE clause so deleting that clause fails exactly its test. - sponsor-asset-report-page: with the real thunks (only getRequest mocked), a fresh mount fetches the current summit and a remount fetches the NEW summit — the direct observation a SummitIdLayout-driven remount produces on a switch. Tests only; no source change. Full suite green (137 suites / 1164 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): guarantee ContentCell img alt is a string (smarcet review) Verified real (not a false positive) by smarcet: alt={row.module?.title} renders an <img> with NO alt attribute when module/title is missing — worse than alt="" for screen readers (they announce the filename/URL). module can be undefined on this row shape (PivotTree's Leaf defends against exactly that). - alt now falls back to row.module?.title || row.module?.component_name || "" (mirrors PivotTree's component_name || title idiom; "" marks it decorative). - ContentCell.test.js: cover the previously-untested image branch with module absent — assert alt="" is present, not an omitted attribute. Also renames a shadowing `thunk` it.each param (no-shadow) in the guard tests. Full suite green (137 suites / 1165 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * refactor(sponsor-reports): address PR #997 review (reducer state, uicore dedup, paging, test prune) Reducer-driven list state (#1): - Move pagination/sort/filter out of component useState into the purchase-details, lines, and sponsor-asset reducers; thunks record them via getRequest's 5th-arg REQUEST payload and components read from props. Mirrors ticket-type-list-page. Adopt uicore components, drop duplicates (#2-#4 + parent sweep): - MuiDropdown for the status/form/payment selects, sponsor-asset status, and PivotSelector; uicore SearchInput in FilterBar; uicore DateTimePicker for the date filters (values kept as YYYY-MM-DD via a UTC boundary conversion, query contract unchanged). - SummaryPanel -> uicore InlineCard; StatusRollupChips -> mui/chip-list. Use the global loading overlay (#5): - Remove per-slice `loading` from all four reports reducers; thunks bracket start/stopLoading so state.baseState.loading + the single AjaxLoader own it. Sponsor-asset keeps a local hasFetched flag for first-mount empty-state suppression. Fold single-consumer helper (#6): - Inline bucketLinesBySponsor into LinesManifestView; delete manifest-grouping.js. Bulk-load sponsor-asset rows with bounded concurrency (#10 / smarcet): - Replace the per_page=5000 fetch (removed ALL_ROWS_PER_PAGE) with page-1-then-parallel paging at per_page=100 via p-limit + Promise.all, matching ticket-actions. Re-check the sequence token inside the pool callback so a superseded invocation's queued jobs cannot fire an identical-page request that would abort the fresh invocation's (under getRequest's page-bearing abort key). Follow-up correctness fixes (Codex review): - toReportDate treats the picker's moment(0) clear-sentinel as "no date" (matches the .unix()||null filter convention) so clearing a date drops the filter instead of sending 1970-01-01. - FilterBar search applies live (debounced) instead of feeding the Apply-gated draft, so a fast type+Apply can't submit a stale value. - Generation-guard hasFetched so a superseded fetch can't flash the empty state. Prune tests to meaningful coverage (parent + #7-#9): - Drop tests that exercised third-party libraries or were low-value (summit-switch, ReportShell, reducer-loading, TierBadge neutral, the manifest-grouping unit test) and reduce sponsor-reports suites to the behavior we own. Add mutation-proof regression tests for the fixes above. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): review follow-ups (lines page size, FilterBar default, dead code) Whole-branch review findings that survived verification: - Restore the Line Items (manifest) default page size to 50. The reducer relocation had set the lines DEFAULT_STATE.perPage to DEFAULT_PER_PAGE (10); the page test seeded perPage:50 in its fixture and masked the wrong default, so the live first fetch pulled 10. Use FIFTY_PER_PAGE + add a reducer test asserting the default (the test that would have caught the regression). - Default FilterBar `value` to a module-level stable {} (EMPTY_VALUE) instead of an inline {}, so omitting the prop no longer makes the value-resync effect loop on a fresh object each render. - Remove dead `query: {}` from the purchase-details reducer DEFAULT_STATE (never read or written). - Remove unused ERROR_CODE_503 constant (503 is handled generically). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): dropdown label overlap + empty Type filter (browser QA) Two issues found while testing the filters in the browser: - MuiDropdown label overlapped the value. The uicore component hardcodes displayEmpty and renders the placeholder (or "Select an option...") in the value area, while a `label` prop becomes a separate InputLabel that never shrinks — so the two overlap. Drive it by `placeholder` (the field name) instead, and restore the combobox's accessible name via SelectDisplayProps={{ "aria-label" }} (a bare aria-label on Select doesn't reach the role="combobox" element). Applies to the Purchase Status / Type / Payment Method selects and the Sponsor Assets status select. - Type filter showed only "Any". The filters endpoint returns forms as { code, name } where name is the (often blank) form_type; the old code dropped every blank-name form, while the table still shows them via each row's prebuilt form.display ("{code} - {name}"). Fall back to the code as the label when the name is blank (dropping only entries with no code), so the Type filter lists the types and matches the table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): translate asset status pills (Copilot review) The Sponsor Assets pivot tree and drill-down rendered the raw status token ("completed", "in_progress") as the StatusPill label, while the same report's tiles and filter use the translated sponsor_reports_page.status_* labels. Translate the label at both asset call sites, with a notFound fallback to the raw token for any status without a key (Media-scoped report only yields the three keyed statuses). Purchase-side pills (OrdersTable, Lines) keep the raw status, which is already human-readable and has no status_* keys. Scope the by_status tiles test to rows:[] so the tile labels don't collide with a seeded row's now-translated pill under the key-echo i18n mock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * refactor(sponsor-reports): address santi approval nits (createAction, tone map, STATUS_KEYS) Three of the five nits from the approving review (the seq-guard one is @smarcet's platform-level call; the FilterBar EMPTY_VALUE one is pushed back on — an inline {} default re-fires the value-resync effect every render): - clearPurchaseDetailsValidation now uses createAction via a thunk, matching the house idiom (summit-actions clearCurrentSummit et al) instead of a hand-rolled plain action object. - Sponsor Assets tiles derive their tone from StatusPill's exported statusTone() instead of a duplicated local TILE_TONE map. - Hoist the displayed-status list to a single exported STATUS_KEYS in build-pivot-tree (which computes the rollup those keys describe); the rollup zero-init, StatusRollupChips, and the asset page's tiles + status filter all consume it instead of holding literal copies. - FilterBar live-search test now OMITS `value`, exercising the module-level EMPTY_VALUE default; regressing it to an inline {} default loops the resync effect ("Maximum update depth exceeded") and blows the test up — mutation-verified against that exact change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): sequence-guard report thunks against stale responses Port the stale-response guard from getSponsorAssetRows to the sibling thunks smarcet flagged (getPurchaseDetailsReport, getPurchaseDetailsLinesReport, getSponsorAssetSponsor): concurrent invocations with different filters, page, sort, or sponsorId carry different getRequest abort keys (only access_token is stripped), so they never cancel each other and whichever response lands last would win, even the older one. Reachable via the pre-startLoading token-refresh window or an A-to-B drill-down navigation, where sponsor A's late response could render under sponsor B's URL. Extract the counter boilerplate into a shared sequenced() factory: each invocation begins a sequence before the token await and gets isCurrent() plus guardedDispatch(), which drops every dispatch (REQUEST, RECEIVE, error handlers, start/stopLoading) once a newer invocation has begun. The newest invocation is always current, so the overlay is always cleared by whichever call finishes last. A stale call's 401 re-login is dropped too (uicore authErrorHandler reaches doLogin by dispatching a thunk through the dispatch it is given) — safe: the fresh call carries the same token, 401s too, and drives the re-login. getSponsorAssetRows is refactored onto the same helper (behavior unchanged; its three existing stale tests pass as-is). Also guard the two filters thunks: safe for same-summit duplicates (identical abort key, getRequest cancels), but a summit switch changes the URL, so the old summit's late filters response would land after the SET_CURRENT_SUMMIT reset and repopulate filterOptions with the wrong summit's options. Add a stale-response test per newly guarded thunk (including the sponsor A over B drill-down and the summit-switch filters case), each mutation-verified: un-guarding the thunk makes its test fail with the stale response landing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
smarcet
pushed a commit
that referenced
this pull request
Jul 6, 2026
…nsors → Reports) (#997) * feat(sponsor-reports): scaffold admin-gated Reports section under Sponsors Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): port actions/reducers/utils + store wiring Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): port shared report components (T.translate, getCSV) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): Purchase Details report (MuiTable, conditional refund tile) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(sponsor-reports): cover CSV+pagination/sort dispatch; drop vestigial SORT_FIELD_MAP Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): Sponsor Assets report + drill-down (rewired routes) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): Reports landing + integration Replace placeholder with two MUI card links (Purchase Details, Sponsor Assets); add breadcrumb; add landing_title i18n key; update layout test for card assertions; 6 new tests, 912 total. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(sponsor-reports): final-review cleanup — drilldown errors, i18n, prop, route test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): drop vestigial export-disabled; route to READ_ERROR Remove *_EXPORT_DISABLED action constants (purchase-details, sponsor-asset, drilldown) and their dead reducer arms. Route onExportDisabled → READ_ERROR in getPurchaseDetailsReport and getSponsorAssetReport thunks (mirrors the already-fixed drilldown), so a 503 export-disabled on a read path clears loading via the same arm as not-found/unauthorized. Remove exportDisabled and exportError from all three reducer DEFAULT_STATEs, drop disabled={exportDisabled} from all three ExportCsvButton usages, and clean up all test references to the deleted constants and unreachable state. Add 503 export-disabled coverage for getPurchaseDetailsReport and getSponsorAssetReport matching the existing drilldown tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): UI polish (fonts, breadcrumb trail, asset spacing) - SummaryPanel tile labels overline -> subtitle1 (bigger; both reports) - GroupByToggle small -> medium + larger button text - SponsorReportsLayout renders a persistent Reports crumb + per-route page crumb so the trail reads .../Sponsors/Reports/<page> (was dropping to .../Sponsors on sub-pages); landing's own crumb removed (no dup) - Sponsor Assets: gap between Report Filters and summary cards - tests updated (landing breadcrumb moved to layout; layout trail test added) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): address CodeRabbit review (PR #997) - guard all report thunks against a missing currentSummit (prevents a stuck spinner from base(currentSummit.id) throwing after startLoading) - formatUsd: treat blank/whitespace strings + non-finite numbers as missing (em dash) instead of $0.00; add reports-money tests - FilterBar: re-sync draft when the committed value prop changes externally - drop redundant routing.test.js (real-layout integration test in sponsor-reports-layout.test.js already covers route ordering) - add PURCHASE_DETAILS_VALIDATION_CLEAR reducer test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): address Copilot review (PR #997) - reports-text: decode uppercase/mixed-case HTML entities (the case-insensitive match was missing the lowercase ENTITIES map); add toPlainText tests - report-errors: export-disabled now dispatches { kind, status, message } to match the other error branches - report-query: coerce sponsorIds to positive integers and drop the rest so the filter never emits sponsor_id==NaN/==0; add buildReportQuery tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(sponsor-reports): request dedicated sponsor-reports scopes (review) Per Sebastian's PR review: summit-admin now requests the new microservice's own scopes (sponsor-reports/read, sponsor-reports/export) instead of relying on the broad reports/all. Adds SPONSOR_REPORTS_SCOPES and wires it into SCOPES (reports/all kept for the legacy summit-reports-api). Requires the IDP to grant these two scopes to the summit-admin client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add purchase-details lines report action Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add purchase-details lines reducer + store registration Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add bucketLinesBySponsor grouping util Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add LinesManifestView collapsible per-line manifest Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add Orders/Line-Items report view toggle Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): wire Line Items manifest view into Purchase Details page Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(sponsor-reports): strip trailing whitespace in LinesManifestView Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): address PR #997 convention review (smarcet) - index.js: import DEFAULT_PER_PAGE from constants (drop local DEFAULT_PAGE_SIZE dup) - index.js: mapDispatchToProps to object shorthand via new clearPurchaseDetailsValidation action creator (removes the raw dispatch prop) - OrdersTable.js: remove dead getOrderRowId export + its test (page maps id inline) - usePrint: move to src/hooks/ per the codebase custom-hook convention - section-csv-query.js: align defense-in-depth comment with the sponsor_id/page_id guard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(sponsor-reports): add lines slice to layout test store (fix CI) The Purchase Details page mapStateToProps reads sponsorReportsPurchaseDetailsLinesState.data; the SponsorReportsLayout test mounts the real page, so its mock store must include that slice. Missing it threw 'Cannot read properties of undefined (reading data)' under the full yarn test run (CI), though the scoped report-suite runs passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): wrap overflowing asset filenames in drilldown links Hashed asset filenames (32-char hex prefix, no spaces) overflowed their card. The link Typography had noWrap (white-space:nowrap). Switch the link to a flex row with overflowWrap:anywhere + minWidth:0 on the filename so the unbroken hash wraps within the card; icons get flexShrink:0 so they don't squash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): readable toggle + summary-tile typography The app sets html root font-size to 62.5% (10px), so rem-based sizes render tiny: the toggle override read 8.75px and subtitle1 tile labels 10px. Pin px instead — toggles to 14px/500 to match the Print/Export action buttons, and the summary-tile labels to 14px. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): visible sponsor avatars + white header-icon theme Add SponsorAvatar: logo-first with a colored-initials fallback so a no-logo (or white-on-white) sponsor is never an invisible blank. Styled to mimic the ReportShell title icon — single primary.light rounded square with white foreground — and used in both group-by views and the drill-down header. Switch the ReportShell title icon glyph to white to match. Also wrap the component-view asset filename (was truncated by noWrap+maxWidth) so the full name is readable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): enable CSV export on the Line Items view Render the Export CSV button on the Line Items (per-line) view of the Purchase Details report, wired to the new backend GET .../purchase-details/lines/csv endpoint. One ExportCsvButton switched by view; lines query strips only pagination (preserves filters incl. the derived include_cancelled). Orders export unchanged. Label stays 'Export CSV'; filename purchase-details-lines-summit-<id>.csv. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): add isPositiveIntId + htmlToPlainText to methods.js Generic helpers move to the shared methods.js home (convention). htmlToPlainText preserves tag-boundary whitespace + decodes entities (htmlToString fuses words). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): co-locate report-query + add shared purchase query builders Move report-query under the feature folder (feature helper convention). Add buildPurchaseQuery/buildPurchaseLinesQuery so the on-screen fetch and the upcoming export thunks share one date/order normalization (no duplication, no regression). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): use methods.htmlToPlainText, drop reports-text util Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): purchase-details CSV export via action thunks Move orders/lines CSV URL+params+filename building into exportPurchaseDetailsCsv/ exportPurchaseDetailsLinesCsv (cf. exportEventRsvpsCSV); page dispatches them from a plain MUI Button. Orders CSV keeps the on-screen sort. base() now reads window.SPONSOR_REPORTS_API_URL directly (drops getReportsApiBaseUrl import). currentSummit removed from component props/mapStateToProps (no longer needed after CSV URL building moved to the thunks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): sponsor-asset CSV export via action thunks; drop ExportCsvButton Add exportSponsorAssetCsv/exportSponsorAssetSectionCsv (section logic folded in, simplified); pages dispatch from plain buttons. Remove the ported ExportCsvButton and section-csv-query util. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): delete reports-api util; wire isPositiveIntId from methods - sponsor-asset-report-page/index.js: repoint isPositiveIntId import to utils/methods - sponsor-asset-drilldown-page/index.js: merge htmlToPlainText + isPositiveIntId into one import from utils/methods (fixes import/no-duplicates) - sponsor-asset-report-page/__tests__: remove jest.mock("reports-api") — pure fn, no mock needed - sponsor-asset-drilldown-page/__tests__: same - layouts/__tests__/sponsor-reports-layout.test.js: remove obsolete reports-api mock + comment - purchase-details-report-page/__tests__: rename stale "ExportCsvButton" test label to "export button" (authorized micro-cleanup; component deleted in Task 5) - git rm src/utils/reports-api.js — no longer consumed by any source or test - 115 suites / 966 tests passing; 0 new lint errors Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): co-locate report-errors + reports-money out of shared utils Both files moved into src/pages/sponsors/sponsor-reports/ per §2 (feature helpers never in shared utils/); test for reports-money co-located alongside. Internal ./constants import in report-errors updated to ../../../utils/constants. formatUsd logic unchanged — uicore/cents contract deferred to PR-B. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): update constants import path in moved report-errors.js ./constants was not re-staged after the path update; this corrects it to ../../../utils/constants (resolves from the new feature folder location). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reports): replace formatUsd with uicore currencyAmountFromCents Backend now emits money fields as integer cents. Drop the bespoke reports-money.js formatter (per santi PR #997 review) and use the platform-wide currencyAmountFromCents from openstack-uicore-foundation. Null guard via inline/local const rather than a new shared helper file. Test fixtures updated to cents input; assertions to no-comma "$X.XX" output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reports): use cents fixtures in purchase-details reducer test Codex follow-up: the reducer passes summary payload through unchanged, so the dollar-string fixtures were not a live bug, but they're stale under the cents contract. Switch total_paid fixtures to integer cents to stay representative. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reports): dedup per-page/ms constants via src/utils/constants - OrdersTable: replace local MS_PER_SECOND (1000) with MILLISECONDS_IN_SECOND and local DEFAULT_PER_PAGE (10) with the shared constant; remove eslint-disable - LinesManifestView: replace magic-number PER_PAGE_OPTIONS [10,25,50,100] and DEFAULT_PER_PAGE (50) with named constants [DEFAULT_PER_PAGE, TWENTY_PER_PAGE, FIFTY_PER_PAGE, MAX_PER_PAGE]; default perPage prop is now FIFTY_PER_PAGE Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * fix(sponsor-reports): return Promise.resolve() on all thunk guard branches Every thunk in sponsor-reports-actions.js that guards against a missing summit returned `undefined` synchronously while the normal code-path returned a Promise — inconsistent for callers using `.then()`/`await`. Changed all ten guard-branch returns to `return Promise.resolve()` so every thunk is uniformly thenable regardless of which branch is taken. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): fold statusTone into StatusPill Moves TONE_BY_STATUS + statusTone() from the standalone statusTone.js into StatusPill.js as a named export. Updates StatusRollupChips.js to import from StatusPill. Deletes statusTone.js and its test file; all branch-coverage assertions migrated into StatusPill.test.js. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(reports): replace hand-rolled formatCheckoutTime with moment Swaps the manual regex + AM/PM math in formatCheckoutTime for moment.unix().utc() / moment.utc() + .format("YYYY-MM-DD h:mm A"). Output is byte-identical for all tested inputs. Removes dead NOON constant and unused MILLISECONDS_IN_SECOND import; keeps ISO_DATE_LENGTH (still needed for the date-only fallback path, and the no-magic-numbers rule requires it named). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): adopt useSnackbarMessage hook for 412 validation toast Replaces the hand-rolled inline MUI <Snackbar>/<Alert> with the existing useSnackbarMessage hook (openstack-uicore-foundation), consistent with payment-view.js and mui-qr-badge-popup.js. Removes TOAST_AUTO_HIDE_MS and the Snackbar import; Alert import kept (still used for readError). Redux clearPurchaseDetailsValidation lifecycle preserved via useEffect. Tests: mocks useSnackbarMessage, asserts errorMessage is called with the correct message (and i18n fallback), clearValidation is dispatched, and errorMessage is not called when validationError is null. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * test(sponsor-reports): mock useSnackbarMessage in layout test The Purchase Details page now calls useSnackbarMessage() (T5); the layout integration test renders that page without the global snackbar provider, so the hook destructured undefined and threw on render. Mock it like the page's own test does. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(T6): move manifest-grouping into reports feature dir bucketLinesBySponsor had exactly one production caller (LinesManifestView). Moving the helper and its test out of src/utils/ and co-locating them with the feature removes the one-off-file-in-shared-utils anti-pattern flagged in PR review. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): T7 — dedup page-size constant + fix default-param-last Edit A: replace local LINES_DEFAULT_PAGE_SIZE = 50 with imported FIFTY_PER_PAGE from utils/constants; Edit B: remove `filters = {}` default on exportPurchaseDetailsCsv — all callers pass an explicit object, buildPurchaseQuery already defaults its own first arg. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): convert nextDayStartIso to moment-timezone Replace hand-rolled new Date / setUTCDate with moment.utc(ymd, "YYYY-MM-DD", true).add(1, "day") for consistency with the rest of the date-formatting in this feature. Output is byte-identical; ISO_DATE_LENGTH removed (now unused). Invalid-input guard returns ymd unchanged rather than throwing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * test(OrdersTable): pin moment.utc offset+malformed contract (T9) Add tests for non-Z offset inputs (-05:00, +05:00) and malformed ISO- like strings to lock current moment.utc() behavior. Backend emits UTC Z only; offset path is inert in production — assertions are documentation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): use repo-native isImageUrl + DEFAULT_CURRENT_PAGE Replace local IMAGE_EXT regex in sponsor-asset-drilldown-page with the repo-canonical isImageUrl from utils/methods (same extensions, also handles query strings). Replace local FIRST_PAGE = 1 in sponsor-asset-report-page with DEFAULT_CURRENT_PAGE from utils/constants (7 references). Replace bare useState(1) / setPage(1) page-init/reset literals in purchase-details- report-page with DEFAULT_CURRENT_PAGE (9 references); orderDir useState(1) (sort direction) is intentionally unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): translate OrdersTable column headers via i18n Replace 7 bare-string header values with T.translate calls; add 5 new col_* keys to en.json; update header assertions in both test files to match key output from the translate mock. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): use DEFAULT_CURRENT_PAGE for page-number defaults Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): use Grid2 in asset drilldown page Replace deprecated MUI v4 Grid (item xs={}) with Grid2 size={{}} API, matching every other file in this PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(sponsor-reports): use object-shorthand mapDispatchToProps in asset pages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): move query builders to actions layer, fold in toOrderParam Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): build report queries inside thunks, not components Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): move report-errors helper to actions layer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * docs(sponsor-reports): fix stale toOrderParam comment in OrdersTable toOrderParam moved to the query thunk in an earlier commit; the column comment no longer describes a page-handler conversion. * feat(sponsor-reports): add ContentTypeToggle (Collected/All) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * feat(sponsor-reports): Collected/All content filter on asset report page Wire ContentTypeToggle (default: collected) into SponsorAssetReportPage — injects moduleType:"Media" on fetch + CSV export when collected, omits it for all; resets page on toggle. Tests: 12/12 (+3 new), full suite 995/995. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * feat(sponsor-reports): suppress download status + Collected/All filter in drilldown Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * fix(sponsor-reports): show empty page sections under All in drilldown Drop-empty-section now applies only in Collected mode; All renders every section as-is, including a page the sponsor never submitted to. * refactor(sponsor-reports): hard-wire sponsor assets to collected (Media); remove toggle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * fix(sponsor-reports): scope section CSV to collected (Media); restore HTML-flatten test - exportSponsorAssetSectionCsv now filters module_type==Media so the per-page Download CSV matches the collected-only view (was an escape path for Document/Info rows). - Restore ContentCell HTML-flatten coverage as a Media text/input row (the prior test used an Info row removed with the toggle teardown). * test(sponsor-reports): strengthen ContentCell flatten test to pin htmlToPlainText Use an input with entities + adjacent tags (<p>Booth A</p><p>B & C</p> → "Booth A B & C") so the test exercises entity-decode + whitespace-collapse + tag-to-space — the behavior that distinguishes htmlToPlainText from stripTags. A regression swapping the helper would now fail. * refactor(sponsor-reports): reuse isPositiveIntId + TWENTY_PER_PAGE; drop port-framing comments Convention-sweep fixes (Codex full-branch pass): - query + section-CSV thunks reuse isPositiveIntId instead of hand-rolled Number.isInteger ID checks; section CSV bails on an invalid id rather than emitting a broadened (whole-sponsor) export - replace local GROUP_PER_PAGE=25 (an arbitrary literal carried from the sponsor-services source, not spec'd) with the existing TWENTY_PER_PAGE - rewrite OrdersGrid/sponsor-services port-framing comments to describe summit-admin behavior only Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): replace report-print.css with MUI GlobalStyles Removes the last standalone stylesheet in the reports feature (legacy in summit-admin per santipalenque). Whole-page print isolation is preserved verbatim via an inline <GlobalStyles> block in ReportShell; the .report-body className anchor is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * refactor(sponsor-reports): extract ContentCell into a shared component Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add pivot axis + named-pivot definitions Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): pure recursive pivot-tree grouping + usePivot memo Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): fetch full flat asset set atomically with last_page paging Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add PivotSelector Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add recursive PivotTree renderer Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): multi-level pivot view replaces single-level grouping Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(sponsor-reports): cover getSponsorAssetRows multi-page paging loop Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): remove orphaned grouped-fetch path + dead exports; fix empty-state Retire getSponsorAssetReport (no callers after the pivot rewire), its dead reducer fields/case and the group_by query branch, drop unused axisKeyOf/axisLabelOf, and default the reducer loading flag true to avoid a no-results flash before first fetch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): drop stale getSponsorAssetRows completions (request token) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * feat(sponsor-reports): route pivot + unknown-bucket labels through i18n Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): trim component pivot label (key/label parity) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): close both stale-dispatch holes in getSponsorAssetRows via guardedDispatch Introduces a single `guardedDispatch` closure (checked against `mySeq` at every call site) to replace the two existing ad-hoc terminal guards. Hole 1 — stuck loading: REQUEST_SPONSOR_ASSET was dispatched after `await getAccessTokenSafely()` without a seq-guard. A stale call that resumed after a newer call had already committed RECEIVE_SPONSOR_ASSET_ROWS would re-flip loading:true with no terminal to clear it. guardedDispatch suppresses the REQUEST dispatch for any superseded invocation. Hole 2 — persistent stale error: fetchPage passed raw dispatch to getRequest, so a stale request's HTTP error handler fired SPONSOR_ASSET_READ_ERROR unguarded — clobbering fresh success state with readError. Changing the innermost `)(dispatch)` to `)(guardedDispatch)` threads the guard through getRequest's error-handler invocation. The RECEIVE and catch-block SPONSOR_ASSET_READ_ERROR terminals now also use guardedDispatch, consolidating all four dispatch sites under one mechanism. Tests added (getSponsorAssetRows): • suppresses REQUEST_SPONSOR_ASSET from a stale call (prevents stuck-loading) • suppresses SPONSOR_ASSET_READ_ERROR from a stale call's error handler Full suite: 1089/1089 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): drop the always-zero N/A status bucket from Sponsor Assets UI The report is scoped to Media assets (server applies module_type==Media), whose completion strategies only ever emit pending/in_progress/completed — never not_applicable. The N/A summary tile and per-group rollup chip were therefore always zero. Remove not_applicable from the tile keys, rollup chips, pivot-tree accumulator, and the now-dead i18n key. Backend contract is untouched (it may still send the key; the FE simply ignores it). StatusPill's generic not_applicable->default tone mapping is left as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add Status filter to Sponsor Assets report (JP request) Single-select Status filter (Any / Completed / In Progress / Pending) in the Sponsor Assets report's FilterBar extraControls slot. Pure FE: the backend SponsorAssetFilter already allows status==, buildReportQuery already emits it, and every row carries status — only the UI control was missing. Reuses STATUS_TILE_KEYS so the options stay in lockstep with the summary tiles. Applies server-side (refetch on Apply), so the summary tiles recompute on the filtered set and CSV export inherits the filter. No backend change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): map Payment Method filter to payment_method== query bracket * feat(sponsor-reports): add payment method + invoice detail columns to orders table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * feat(sponsor-reports): add Payment Method filter to Purchase Details report Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(sponsor-reports): tighten finance-column assertions (all 4 non-sortable, exact em-dash count) * refactor(sponsor-reports): rework read-error handling + address PR #997 review Addresses the smarcet + santipalenque CHANGES_REQUESTED on the Sponsor Reports FE port, plus a whole-PR convention sweep and correctness pass (each verified with an independent review). Error handling: - Delete sponsor-reports-errors.js; fold a module-private reportReadErrorHandler into sponsor-reports-actions.js, matching the per-feature handler precedent in sponsor-cart-actions.js / summit-actions.js (src/actions holds only action files). - Delegate 401 to uicore authErrorHandler so its isClearingSessionState guard dedupes re-login across the paginated getSponsorAssetRows fetch (was calling doLogin() raw per page). - Drop the dead classifyReportError taxonomy (onExportDisabled was wired to the same action as onReadError everywhere; the kind zoo was unused). Keep only what pages consume: inline readError.message, kind:"not-found" for the drilldown 404, and the Purchase Details 412 validation toast. Reports keep inline error bodies (not the app snackbar) by design. - Gate the getSponsorAssetRows catch to genuine non-HTTP exceptions (e instanceof Error): uicore rejects HTTP failures with a plain {err,res,...} object after invoking the handler, so the old unconditional catch flashed an inline error over the 401 reauth and clobbered the handler's server message on 403/500. Correctness: - Escape the five string filter[] operands (status, form_code, payment_method, module_type, media_request_type) with escapeFilterValue, per the FN filtering convention (cf. sponsor-cart-actions.js / event-rsvp-actions.js). Numeric ids, ISO dates, and the top-level search param stay literal. - getSponsorAssetRows: bail a superseded invocation before it fires a request (after the token await and in the pagination loop). uicore getRequest aborts the in-flight same-URL request, so a stale call could otherwise cancel the current load. Complements the existing seq-guard on dispatches. - Drilldown empty-state: guard on visiblePages.length (Media-filtered) instead of pages.length, so a sponsor whose pages hold only non-Media modules shows the no-submissions panel rather than an empty body. Convention sweep: - Fold sponsor-reports-query.js into sponsor-reports-actions.js (only non-*-actions file in src/actions, imported only there); builders stay exported for their tests. - Move usePivot to src/hooks (canonical hook location, cf. usePrint). - Remove 6 dead i18n keys under sponsor_reports_page (live pivot_unnamed_component untouched). - Optional-chaining sweep for row.module to match PivotTree's defensive idiom: ContentCell (2) + sponsor-asset-drilldown-page (4). Rewrites the handler's tests as thunk-level integration tests; adds live-401 / live-403 asset-fetch and no-submissions coverage. Full suite green (131 suites / 1130 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): pattern-audit follow-ups (filter scope, 404 route, hook fold) Three issues surfaced by a whole-PR pattern audit against summit-admin exemplars. - Payment Method filter silently no-op'd on the Line Items view. The Purchase Details page shares one filters object across the orders/lines toggle, but the lines endpoint's filter set omits payment_method (order-level attribute) and BaseFilter silently skips unknown AND-filter fields — so selecting a payment method returned unfiltered lines. Gate the control to the orders view (mirrors search being view-specific) and strip paymentMethod in buildPurchaseLinesQuery so a value carried over from the orders view can't emit a dead clause. Orders view is unchanged (buildPurchaseQuery still emits payment_method). - Reports layout had no catch-all route, so an unknown /reports/... path rendered only the breadcrumb shell. Add <Route component={NoMatchPage} /> last in the Switch, matching sponsor-layout/event-layout. Eager import (not lazy) to match this layout's own convention — it eager-imports its pages and has no Suspense. - Fold usePivot into its only consumer (sponsor-asset-report-page) and delete the hook. A single-export helper with one consumer belongs in the consumer, not the shared src/hooks dir (react-frontend.md §2). usePrint stays in src/hooks — it's generic (window.print) and used by all three report pages. Full suite green (131 suites / 1131 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * test(sponsor-reports): regression tests for the summit-gate rebuttal Encodes the invariant behind the "mount-only effect never re-runs on summit change → stuck empty" review concern, so a future refactor that breaks it fails CI instead of silently reintroducing the bug. - sponsor-reports-actions: mount fetches (getSponsorAssetFilters/Rows, getPurchaseDetailsFilters) no-op with no current summit, across null / {} / { id: 0 } (a weaker `!currentSummit` guard fails the {} and { id: 0 } cases). - summit-id-layout: the gate renders nothing unless currentSummit.id && summitId === currentSummit.id && !loading && hasLoaded, with each negative case isolating ONE clause so deleting that clause fails exactly its test. - sponsor-asset-report-page: with the real thunks (only getRequest mocked), a fresh mount fetches the current summit and a remount fetches the NEW summit — the direct observation a SummitIdLayout-driven remount produces on a switch. Tests only; no source change. Full suite green (137 suites / 1164 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): guarantee ContentCell img alt is a string (smarcet review) Verified real (not a false positive) by smarcet: alt={row.module?.title} renders an <img> with NO alt attribute when module/title is missing — worse than alt="" for screen readers (they announce the filename/URL). module can be undefined on this row shape (PivotTree's Leaf defends against exactly that). - alt now falls back to row.module?.title || row.module?.component_name || "" (mirrors PivotTree's component_name || title idiom; "" marks it decorative). - ContentCell.test.js: cover the previously-untested image branch with module absent — assert alt="" is present, not an omitted attribute. Also renames a shadowing `thunk` it.each param (no-shadow) in the guard tests. Full suite green (137 suites / 1165 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * refactor(sponsor-reports): address PR #997 review (reducer state, uicore dedup, paging) Reducer-driven list state (#1): - Move pagination/sort/filter out of component useState into the purchase-details, lines, and sponsor-asset reducers; thunks record them via getRequest's 5th-arg REQUEST payload and components read from props. Mirrors ticket-type-list-page. Adopt uicore components, drop duplicates (#2-#4 + parent sweep): - MuiDropdown for the status/form/payment selects, sponsor-asset status, and PivotSelector; uicore SearchInput in FilterBar; uicore DateTimePicker for the date filters (values kept as YYYY-MM-DD via a UTC boundary conversion, query contract unchanged). - SummaryPanel -> uicore InlineCard; StatusRollupChips -> mui/chip-list. Use the global loading overlay (#5): - Remove per-slice `loading` from all four reports reducers; thunks bracket start/stopLoading so state.baseState.loading + the single AjaxLoader own it. Sponsor-asset keeps a local hasFetched flag for first-mount empty-state suppression. Fold single-consumer helper (#6): - Inline bucketLinesBySponsor into LinesManifestView; delete manifest-grouping.js. Bulk-load sponsor-asset rows with bounded concurrency (#10 / smarcet): - Replace the per_page=5000 fetch (removed ALL_ROWS_PER_PAGE) with page-1-then-parallel paging at per_page=100 via p-limit + Promise.all, matching ticket-actions. Re-check the sequence token inside the pool callback so a superseded invocation's queued jobs cannot fire an identical-page request that would abort the fresh invocation's (under getRequest's page-bearing abort key). Follow-up correctness fixes (Codex review): - toReportDate treats the picker's moment(0) clear-sentinel as "no date" (matches the .unix()||null filter convention) so clearing a date drops the filter instead of sending 1970-01-01. - FilterBar search applies live (debounced) instead of feeding the Apply-gated draft, so a fast type+Apply can't submit a stale value. - Generation-guard hasFetched so a superseded fetch can't flash the empty state. Prune tests to meaningful coverage (parent + #7-#9): - Drop tests that exercised third-party libraries or were low-value (summit-switch, ReportShell, reducer-loading, TierBadge neutral, the manifest-grouping unit test) and reduce sponsor-reports suites to the behavior we own. Add mutation-proof regression tests for the fixes above. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): review follow-ups (lines page size, FilterBar default, dead code) Whole-branch review findings that survived verification: - Restore the Line Items (manifest) default page size to 50. The reducer relocation had set the lines DEFAULT_STATE.perPage to DEFAULT_PER_PAGE (10); the page test seeded perPage:50 in its fixture and masked the wrong default, so the live first fetch pulled 10. Use FIFTY_PER_PAGE + add a reducer test asserting the default (the test that would have caught the regression). - Default FilterBar `value` to a module-level stable {} (EMPTY_VALUE) instead of an inline {}, so omitting the prop no longer makes the value-resync effect loop on a fresh object each render. - Remove dead `query: {}` from the purchase-details reducer DEFAULT_STATE (never read or written). - Remove unused ERROR_CODE_503 constant (503 is handled generically). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): dropdown label overlap + empty Type filter (browser QA) Two issues found while testing the filters in the browser: - MuiDropdown label overlapped the value. The uicore component hardcodes displayEmpty and renders the placeholder (or "Select an option...") in the value area, while a `label` prop becomes a separate InputLabel that never shrinks — so the two overlap. Drive it by `placeholder` (the field name) instead, and restore the combobox's accessible name via SelectDisplayProps={{ "aria-label" }} (a bare aria-label on Select doesn't reach the role="combobox" element). Applies to the Purchase Status / Type / Payment Method selects and the Sponsor Assets status select. - Type filter showed only "Any". The filters endpoint returns forms as { code, name } where name is the (often blank) form_type; the old code dropped every blank-name form, while the table still shows them via each row's prebuilt form.display ("{code} - {name}"). Fall back to the code as the label when the name is blank (dropping only entries with no code), so the Type filter lists the types and matches the table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): translate asset status pills (Copilot review) The Sponsor Assets pivot tree and drill-down rendered the raw status token ("completed", "in_progress") as the StatusPill label, while the same report's tiles and filter use the translated sponsor_reports_page.status_* labels. Translate the label at both asset call sites, with a notFound fallback to the raw token for any status without a key (Media-scoped report only yields the three keyed statuses). Purchase-side pills (OrdersTable, Lines) keep the raw status, which is already human-readable and has no status_* keys. Scope the by_status tiles test to rows:[] so the tile labels don't collide with a seeded row's now-translated pill under the key-echo i18n mock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * refactor(sponsor-reports): address santi approval nits (createAction, tone map, STATUS_KEYS) Three of the five nits from the approving review (the seq-guard one is @smarcet's platform-level call; the FilterBar EMPTY_VALUE one is pushed back on — an inline {} default re-fires the value-resync effect every render): - clearPurchaseDetailsValidation now uses createAction via a thunk, matching the house idiom (summit-actions clearCurrentSummit et al) instead of a hand-rolled plain action object. - Sponsor Assets tiles derive their tone from StatusPill's exported statusTone() instead of a duplicated local TILE_TONE map. - Hoist the displayed-status list to a single exported STATUS_KEYS in build-pivot-tree (which computes the rollup those keys describe); the rollup zero-init, StatusRollupChips, and the asset page's tiles + status filter all consume it instead of holding literal copies. - FilterBar live-search test now OMITS `value`, exercising the module-level EMPTY_VALUE default; regressing it to an inline {} default loops the resync effect ("Maximum update depth exceeded") and blows the test up — mutation-verified against that exact change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): sequence-guard report thunks against stale responses Port the stale-response guard from getSponsorAssetRows to the sibling thunks smarcet flagged (getPurchaseDetailsReport, getPurchaseDetailsLinesReport, getSponsorAssetSponsor): concurrent invocations with different filters, page, sort, or sponsorId carry different getRequest abort keys (only access_token is stripped), so they never cancel each other and whichever response lands last would win, even the older one. Reachable via the pre-startLoading token-refresh window or an A-to-B drill-down navigation, where sponsor A's late response could render under sponsor B's URL. Extract the counter boilerplate into a shared sequenced() factory: each invocation begins a sequence before the token await and gets isCurrent() plus guardedDispatch(), which drops every dispatch (REQUEST, RECEIVE, error handlers, start/stopLoading) once a newer invocation has begun. The newest invocation is always current, so the overlay is always cleared by whichever call finishes last. A stale call's 401 re-login is dropped too (uicore authErrorHandler reaches doLogin by dispatching a thunk through the dispatch it is given) — safe: the fresh call carries the same token, 401s too, and drives the re-login. getSponsorAssetRows is refactored onto the same helper (behavior unchanged; its three existing stale tests pass as-is). Also guard the two filters thunks: safe for same-summit duplicates (identical abort key, getRequest cancels), but a summit switch changes the URL, so the old summit's late filters response would land after the SET_CURRENT_SUMMIT reset and repopulate filterOptions with the wrong summit's options. Add a stale-response test per newly guarded thunk (including the sponsor A over B drill-down and the summit-switch filters case), each mutation-verified: un-guarding the thunk makes its test fail with the stale response landing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
smarcet
pushed a commit
that referenced
this pull request
Jul 16, 2026
…nsors → Reports) (#997) * feat(sponsor-reports): scaffold admin-gated Reports section under Sponsors Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): port actions/reducers/utils + store wiring Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): port shared report components (T.translate, getCSV) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): Purchase Details report (MuiTable, conditional refund tile) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(sponsor-reports): cover CSV+pagination/sort dispatch; drop vestigial SORT_FIELD_MAP Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): Sponsor Assets report + drill-down (rewired routes) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): Reports landing + integration Replace placeholder with two MUI card links (Purchase Details, Sponsor Assets); add breadcrumb; add landing_title i18n key; update layout test for card assertions; 6 new tests, 912 total. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(sponsor-reports): final-review cleanup — drilldown errors, i18n, prop, route test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): drop vestigial export-disabled; route to READ_ERROR Remove *_EXPORT_DISABLED action constants (purchase-details, sponsor-asset, drilldown) and their dead reducer arms. Route onExportDisabled → READ_ERROR in getPurchaseDetailsReport and getSponsorAssetReport thunks (mirrors the already-fixed drilldown), so a 503 export-disabled on a read path clears loading via the same arm as not-found/unauthorized. Remove exportDisabled and exportError from all three reducer DEFAULT_STATEs, drop disabled={exportDisabled} from all three ExportCsvButton usages, and clean up all test references to the deleted constants and unreachable state. Add 503 export-disabled coverage for getPurchaseDetailsReport and getSponsorAssetReport matching the existing drilldown tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): UI polish (fonts, breadcrumb trail, asset spacing) - SummaryPanel tile labels overline -> subtitle1 (bigger; both reports) - GroupByToggle small -> medium + larger button text - SponsorReportsLayout renders a persistent Reports crumb + per-route page crumb so the trail reads .../Sponsors/Reports/<page> (was dropping to .../Sponsors on sub-pages); landing's own crumb removed (no dup) - Sponsor Assets: gap between Report Filters and summary cards - tests updated (landing breadcrumb moved to layout; layout trail test added) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): address CodeRabbit review (PR #997) - guard all report thunks against a missing currentSummit (prevents a stuck spinner from base(currentSummit.id) throwing after startLoading) - formatUsd: treat blank/whitespace strings + non-finite numbers as missing (em dash) instead of $0.00; add reports-money tests - FilterBar: re-sync draft when the committed value prop changes externally - drop redundant routing.test.js (real-layout integration test in sponsor-reports-layout.test.js already covers route ordering) - add PURCHASE_DETAILS_VALIDATION_CLEAR reducer test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): address Copilot review (PR #997) - reports-text: decode uppercase/mixed-case HTML entities (the case-insensitive match was missing the lowercase ENTITIES map); add toPlainText tests - report-errors: export-disabled now dispatches { kind, status, message } to match the other error branches - report-query: coerce sponsorIds to positive integers and drop the rest so the filter never emits sponsor_id==NaN/==0; add buildReportQuery tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(sponsor-reports): request dedicated sponsor-reports scopes (review) Per Sebastian's PR review: summit-admin now requests the new microservice's own scopes (sponsor-reports/read, sponsor-reports/export) instead of relying on the broad reports/all. Adds SPONSOR_REPORTS_SCOPES and wires it into SCOPES (reports/all kept for the legacy summit-reports-api). Requires the IDP to grant these two scopes to the summit-admin client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add purchase-details lines report action Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add purchase-details lines reducer + store registration Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add bucketLinesBySponsor grouping util Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add LinesManifestView collapsible per-line manifest Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add Orders/Line-Items report view toggle Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): wire Line Items manifest view into Purchase Details page Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(sponsor-reports): strip trailing whitespace in LinesManifestView Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): address PR #997 convention review (smarcet) - index.js: import DEFAULT_PER_PAGE from constants (drop local DEFAULT_PAGE_SIZE dup) - index.js: mapDispatchToProps to object shorthand via new clearPurchaseDetailsValidation action creator (removes the raw dispatch prop) - OrdersTable.js: remove dead getOrderRowId export + its test (page maps id inline) - usePrint: move to src/hooks/ per the codebase custom-hook convention - section-csv-query.js: align defense-in-depth comment with the sponsor_id/page_id guard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(sponsor-reports): add lines slice to layout test store (fix CI) The Purchase Details page mapStateToProps reads sponsorReportsPurchaseDetailsLinesState.data; the SponsorReportsLayout test mounts the real page, so its mock store must include that slice. Missing it threw 'Cannot read properties of undefined (reading data)' under the full yarn test run (CI), though the scoped report-suite runs passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): wrap overflowing asset filenames in drilldown links Hashed asset filenames (32-char hex prefix, no spaces) overflowed their card. The link Typography had noWrap (white-space:nowrap). Switch the link to a flex row with overflowWrap:anywhere + minWidth:0 on the filename so the unbroken hash wraps within the card; icons get flexShrink:0 so they don't squash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): readable toggle + summary-tile typography The app sets html root font-size to 62.5% (10px), so rem-based sizes render tiny: the toggle override read 8.75px and subtitle1 tile labels 10px. Pin px instead — toggles to 14px/500 to match the Print/Export action buttons, and the summary-tile labels to 14px. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): visible sponsor avatars + white header-icon theme Add SponsorAvatar: logo-first with a colored-initials fallback so a no-logo (or white-on-white) sponsor is never an invisible blank. Styled to mimic the ReportShell title icon — single primary.light rounded square with white foreground — and used in both group-by views and the drill-down header. Switch the ReportShell title icon glyph to white to match. Also wrap the component-view asset filename (was truncated by noWrap+maxWidth) so the full name is readable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): enable CSV export on the Line Items view Render the Export CSV button on the Line Items (per-line) view of the Purchase Details report, wired to the new backend GET .../purchase-details/lines/csv endpoint. One ExportCsvButton switched by view; lines query strips only pagination (preserves filters incl. the derived include_cancelled). Orders export unchanged. Label stays 'Export CSV'; filename purchase-details-lines-summit-<id>.csv. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): add isPositiveIntId + htmlToPlainText to methods.js Generic helpers move to the shared methods.js home (convention). htmlToPlainText preserves tag-boundary whitespace + decodes entities (htmlToString fuses words). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): co-locate report-query + add shared purchase query builders Move report-query under the feature folder (feature helper convention). Add buildPurchaseQuery/buildPurchaseLinesQuery so the on-screen fetch and the upcoming export thunks share one date/order normalization (no duplication, no regression). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): use methods.htmlToPlainText, drop reports-text util Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): purchase-details CSV export via action thunks Move orders/lines CSV URL+params+filename building into exportPurchaseDetailsCsv/ exportPurchaseDetailsLinesCsv (cf. exportEventRsvpsCSV); page dispatches them from a plain MUI Button. Orders CSV keeps the on-screen sort. base() now reads window.SPONSOR_REPORTS_API_URL directly (drops getReportsApiBaseUrl import). currentSummit removed from component props/mapStateToProps (no longer needed after CSV URL building moved to the thunks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): sponsor-asset CSV export via action thunks; drop ExportCsvButton Add exportSponsorAssetCsv/exportSponsorAssetSectionCsv (section logic folded in, simplified); pages dispatch from plain buttons. Remove the ported ExportCsvButton and section-csv-query util. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): delete reports-api util; wire isPositiveIntId from methods - sponsor-asset-report-page/index.js: repoint isPositiveIntId import to utils/methods - sponsor-asset-drilldown-page/index.js: merge htmlToPlainText + isPositiveIntId into one import from utils/methods (fixes import/no-duplicates) - sponsor-asset-report-page/__tests__: remove jest.mock("reports-api") — pure fn, no mock needed - sponsor-asset-drilldown-page/__tests__: same - layouts/__tests__/sponsor-reports-layout.test.js: remove obsolete reports-api mock + comment - purchase-details-report-page/__tests__: rename stale "ExportCsvButton" test label to "export button" (authorized micro-cleanup; component deleted in Task 5) - git rm src/utils/reports-api.js — no longer consumed by any source or test - 115 suites / 966 tests passing; 0 new lint errors Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): co-locate report-errors + reports-money out of shared utils Both files moved into src/pages/sponsors/sponsor-reports/ per §2 (feature helpers never in shared utils/); test for reports-money co-located alongside. Internal ./constants import in report-errors updated to ../../../utils/constants. formatUsd logic unchanged — uicore/cents contract deferred to PR-B. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): update constants import path in moved report-errors.js ./constants was not re-staged after the path update; this corrects it to ../../../utils/constants (resolves from the new feature folder location). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reports): replace formatUsd with uicore currencyAmountFromCents Backend now emits money fields as integer cents. Drop the bespoke reports-money.js formatter (per santi PR #997 review) and use the platform-wide currencyAmountFromCents from openstack-uicore-foundation. Null guard via inline/local const rather than a new shared helper file. Test fixtures updated to cents input; assertions to no-comma "$X.XX" output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reports): use cents fixtures in purchase-details reducer test Codex follow-up: the reducer passes summary payload through unchanged, so the dollar-string fixtures were not a live bug, but they're stale under the cents contract. Switch total_paid fixtures to integer cents to stay representative. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reports): dedup per-page/ms constants via src/utils/constants - OrdersTable: replace local MS_PER_SECOND (1000) with MILLISECONDS_IN_SECOND and local DEFAULT_PER_PAGE (10) with the shared constant; remove eslint-disable - LinesManifestView: replace magic-number PER_PAGE_OPTIONS [10,25,50,100] and DEFAULT_PER_PAGE (50) with named constants [DEFAULT_PER_PAGE, TWENTY_PER_PAGE, FIFTY_PER_PAGE, MAX_PER_PAGE]; default perPage prop is now FIFTY_PER_PAGE Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * fix(sponsor-reports): return Promise.resolve() on all thunk guard branches Every thunk in sponsor-reports-actions.js that guards against a missing summit returned `undefined` synchronously while the normal code-path returned a Promise — inconsistent for callers using `.then()`/`await`. Changed all ten guard-branch returns to `return Promise.resolve()` so every thunk is uniformly thenable regardless of which branch is taken. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): fold statusTone into StatusPill Moves TONE_BY_STATUS + statusTone() from the standalone statusTone.js into StatusPill.js as a named export. Updates StatusRollupChips.js to import from StatusPill. Deletes statusTone.js and its test file; all branch-coverage assertions migrated into StatusPill.test.js. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(reports): replace hand-rolled formatCheckoutTime with moment Swaps the manual regex + AM/PM math in formatCheckoutTime for moment.unix().utc() / moment.utc() + .format("YYYY-MM-DD h:mm A"). Output is byte-identical for all tested inputs. Removes dead NOON constant and unused MILLISECONDS_IN_SECOND import; keeps ISO_DATE_LENGTH (still needed for the date-only fallback path, and the no-magic-numbers rule requires it named). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): adopt useSnackbarMessage hook for 412 validation toast Replaces the hand-rolled inline MUI <Snackbar>/<Alert> with the existing useSnackbarMessage hook (openstack-uicore-foundation), consistent with payment-view.js and mui-qr-badge-popup.js. Removes TOAST_AUTO_HIDE_MS and the Snackbar import; Alert import kept (still used for readError). Redux clearPurchaseDetailsValidation lifecycle preserved via useEffect. Tests: mocks useSnackbarMessage, asserts errorMessage is called with the correct message (and i18n fallback), clearValidation is dispatched, and errorMessage is not called when validationError is null. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * test(sponsor-reports): mock useSnackbarMessage in layout test The Purchase Details page now calls useSnackbarMessage() (T5); the layout integration test renders that page without the global snackbar provider, so the hook destructured undefined and threw on render. Mock it like the page's own test does. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(T6): move manifest-grouping into reports feature dir bucketLinesBySponsor had exactly one production caller (LinesManifestView). Moving the helper and its test out of src/utils/ and co-locating them with the feature removes the one-off-file-in-shared-utils anti-pattern flagged in PR review. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): T7 — dedup page-size constant + fix default-param-last Edit A: replace local LINES_DEFAULT_PAGE_SIZE = 50 with imported FIFTY_PER_PAGE from utils/constants; Edit B: remove `filters = {}` default on exportPurchaseDetailsCsv — all callers pass an explicit object, buildPurchaseQuery already defaults its own first arg. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): convert nextDayStartIso to moment-timezone Replace hand-rolled new Date / setUTCDate with moment.utc(ymd, "YYYY-MM-DD", true).add(1, "day") for consistency with the rest of the date-formatting in this feature. Output is byte-identical; ISO_DATE_LENGTH removed (now unused). Invalid-input guard returns ymd unchanged rather than throwing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * test(OrdersTable): pin moment.utc offset+malformed contract (T9) Add tests for non-Z offset inputs (-05:00, +05:00) and malformed ISO- like strings to lock current moment.utc() behavior. Backend emits UTC Z only; offset path is inert in production — assertions are documentation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): use repo-native isImageUrl + DEFAULT_CURRENT_PAGE Replace local IMAGE_EXT regex in sponsor-asset-drilldown-page with the repo-canonical isImageUrl from utils/methods (same extensions, also handles query strings). Replace local FIRST_PAGE = 1 in sponsor-asset-report-page with DEFAULT_CURRENT_PAGE from utils/constants (7 references). Replace bare useState(1) / setPage(1) page-init/reset literals in purchase-details- report-page with DEFAULT_CURRENT_PAGE (9 references); orderDir useState(1) (sort direction) is intentionally unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): translate OrdersTable column headers via i18n Replace 7 bare-string header values with T.translate calls; add 5 new col_* keys to en.json; update header assertions in both test files to match key output from the translate mock. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): use DEFAULT_CURRENT_PAGE for page-number defaults Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): use Grid2 in asset drilldown page Replace deprecated MUI v4 Grid (item xs={}) with Grid2 size={{}} API, matching every other file in this PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(sponsor-reports): use object-shorthand mapDispatchToProps in asset pages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): move query builders to actions layer, fold in toOrderParam Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): build report queries inside thunks, not components Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): move report-errors helper to actions layer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * docs(sponsor-reports): fix stale toOrderParam comment in OrdersTable toOrderParam moved to the query thunk in an earlier commit; the column comment no longer describes a page-handler conversion. * feat(sponsor-reports): add ContentTypeToggle (Collected/All) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * feat(sponsor-reports): Collected/All content filter on asset report page Wire ContentTypeToggle (default: collected) into SponsorAssetReportPage — injects moduleType:"Media" on fetch + CSV export when collected, omits it for all; resets page on toggle. Tests: 12/12 (+3 new), full suite 995/995. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * feat(sponsor-reports): suppress download status + Collected/All filter in drilldown Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * fix(sponsor-reports): show empty page sections under All in drilldown Drop-empty-section now applies only in Collected mode; All renders every section as-is, including a page the sponsor never submitted to. * refactor(sponsor-reports): hard-wire sponsor assets to collected (Media); remove toggle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * fix(sponsor-reports): scope section CSV to collected (Media); restore HTML-flatten test - exportSponsorAssetSectionCsv now filters module_type==Media so the per-page Download CSV matches the collected-only view (was an escape path for Document/Info rows). - Restore ContentCell HTML-flatten coverage as a Media text/input row (the prior test used an Info row removed with the toggle teardown). * test(sponsor-reports): strengthen ContentCell flatten test to pin htmlToPlainText Use an input with entities + adjacent tags (<p>Booth A</p><p>B & C</p> → "Booth A B & C") so the test exercises entity-decode + whitespace-collapse + tag-to-space — the behavior that distinguishes htmlToPlainText from stripTags. A regression swapping the helper would now fail. * refactor(sponsor-reports): reuse isPositiveIntId + TWENTY_PER_PAGE; drop port-framing comments Convention-sweep fixes (Codex full-branch pass): - query + section-CSV thunks reuse isPositiveIntId instead of hand-rolled Number.isInteger ID checks; section CSV bails on an invalid id rather than emitting a broadened (whole-sponsor) export - replace local GROUP_PER_PAGE=25 (an arbitrary literal carried from the sponsor-services source, not spec'd) with the existing TWENTY_PER_PAGE - rewrite OrdersGrid/sponsor-services port-framing comments to describe summit-admin behavior only Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1oFJHczgo13Z6MnUUHjwV * refactor(sponsor-reports): replace report-print.css with MUI GlobalStyles Removes the last standalone stylesheet in the reports feature (legacy in summit-admin per santipalenque). Whole-page print isolation is preserved verbatim via an inline <GlobalStyles> block in ReportShell; the .report-body className anchor is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * refactor(sponsor-reports): extract ContentCell into a shared component Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add pivot axis + named-pivot definitions Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): pure recursive pivot-tree grouping + usePivot memo Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): fetch full flat asset set atomically with last_page paging Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add PivotSelector Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add recursive PivotTree renderer Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): multi-level pivot view replaces single-level grouping Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(sponsor-reports): cover getSponsorAssetRows multi-page paging loop Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sponsor-reports): remove orphaned grouped-fetch path + dead exports; fix empty-state flash Retire getSponsorAssetReport (no callers after the pivot rewire), its dead reducer fields/case and the group_by query branch, drop unused axisKeyOf/axisLabelOf, and default the reducer loading flag true to avoid a no-results flash before first fetch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sponsor-reports): drop stale getSponsorAssetRows completions (request token) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * feat(sponsor-reports): route pivot + unknown-bucket labels through i18n Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): trim component pivot label (key/label parity) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): close both stale-dispatch holes in getSponsorAssetRows via guardedDispatch Introduces a single `guardedDispatch` closure (checked against `mySeq` at every call site) to replace the two existing ad-hoc terminal guards. Hole 1 — stuck loading: REQUEST_SPONSOR_ASSET was dispatched after `await getAccessTokenSafely()` without a seq-guard. A stale call that resumed after a newer call had already committed RECEIVE_SPONSOR_ASSET_ROWS would re-flip loading:true with no terminal to clear it. guardedDispatch suppresses the REQUEST dispatch for any superseded invocation. Hole 2 — persistent stale error: fetchPage passed raw dispatch to getRequest, so a stale request's HTTP error handler fired SPONSOR_ASSET_READ_ERROR unguarded — clobbering fresh success state with readError. Changing the innermost `)(dispatch)` to `)(guardedDispatch)` threads the guard through getRequest's error-handler invocation. The RECEIVE and catch-block SPONSOR_ASSET_READ_ERROR terminals now also use guardedDispatch, consolidating all four dispatch sites under one mechanism. Tests added (getSponsorAssetRows): • suppresses REQUEST_SPONSOR_ASSET from a stale call (prevents stuck-loading) • suppresses SPONSOR_ASSET_READ_ERROR from a stale call's error handler Full suite: 1089/1089 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): drop the always-zero N/A status bucket from Sponsor Assets UI The report is scoped to Media assets (server applies module_type==Media), whose completion strategies only ever emit pending/in_progress/completed — never not_applicable. The N/A summary tile and per-group rollup chip were therefore always zero. Remove not_applicable from the tile keys, rollup chips, pivot-tree accumulator, and the now-dead i18n key. Backend contract is untouched (it may still send the key; the FE simply ignores it). StatusPill's generic not_applicable->default tone mapping is left as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): add Status filter to Sponsor Assets report (JP request) Single-select Status filter (Any / Completed / In Progress / Pending) in the Sponsor Assets report's FilterBar extraControls slot. Pure FE: the backend SponsorAssetFilter already allows status==, buildReportQuery already emits it, and every row carries status — only the UI control was missing. Reuses STATUS_TILE_KEYS so the options stay in lockstep with the summary tiles. Applies server-side (refetch on Apply), so the summary tiles recompute on the filtered set and CSV export inherits the filter. No backend change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sponsor-reports): map Payment Method filter to payment_method== query bracket * feat(sponsor-reports): add payment method + invoice detail columns to Purchase Details orders table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * feat(sponsor-reports): add Payment Method filter to Purchase Details report Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(sponsor-reports): tighten finance-column assertions (all 4 non-sortable, exact em-dash count) * refactor(sponsor-reports): rework read-error handling + address PR #997 review Addresses the smarcet + santipalenque CHANGES_REQUESTED on the Sponsor Reports FE port, plus a whole-PR convention sweep and correctness pass (each verified with an independent review). Error handling: - Delete sponsor-reports-errors.js; fold a module-private reportReadErrorHandler into sponsor-reports-actions.js, matching the per-feature handler precedent in sponsor-cart-actions.js / summit-actions.js (src/actions holds only action files). - Delegate 401 to uicore authErrorHandler so its isClearingSessionState guard dedupes re-login across the paginated getSponsorAssetRows fetch (was calling doLogin() raw per page). - Drop the dead classifyReportError taxonomy (onExportDisabled was wired to the same action as onReadError everywhere; the kind zoo was unused). Keep only what pages consume: inline readError.message, kind:"not-found" for the drilldown 404, and the Purchase Details 412 validation toast. Reports keep inline error bodies (not the app snackbar) by design. - Gate the getSponsorAssetRows catch to genuine non-HTTP exceptions (e instanceof Error): uicore rejects HTTP failures with a plain {err,res,...} object after invoking the handler, so the old unconditional catch flashed an inline error over the 401 reauth and clobbered the handler's server message on 403/500. Correctness: - Escape the five string filter[] operands (status, form_code, payment_method, module_type, media_request_type) with escapeFilterValue, per the FN filtering convention (cf. sponsor-cart-actions.js / event-rsvp-actions.js). Numeric ids, ISO dates, and the top-level search param stay literal. - getSponsorAssetRows: bail a superseded invocation before it fires a request (after the token await and in the pagination loop). uicore getRequest aborts the in-flight same-URL request, so a stale call could otherwise cancel the current load. Complements the existing seq-guard on dispatches. - Drilldown empty-state: guard on visiblePages.length (Media-filtered) instead of pages.length, so a sponsor whose pages hold only non-Media modules shows the no-submissions panel rather than an empty body. Convention sweep: - Fold sponsor-reports-query.js into sponsor-reports-actions.js (only non-*-actions file in src/actions, imported only there); builders stay exported for their tests. - Move usePivot to src/hooks (canonical hook location, cf. usePrint). - Remove 6 dead i18n keys under sponsor_reports_page (live pivot_unnamed_component untouched). - Optional-chaining sweep for row.module to match PivotTree's defensive idiom: ContentCell (2) + sponsor-asset-drilldown-page (4). Rewrites the handler's tests as thunk-level integration tests; adds live-401 / live-403 asset-fetch and no-submissions coverage. Full suite green (131 suites / 1130 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): pattern-audit follow-ups (filter scope, 404 route, hook fold) Three issues surfaced by a whole-PR pattern audit against summit-admin exemplars. - Payment Method filter silently no-op'd on the Line Items view. The Purchase Details page shares one filters object across the orders/lines toggle, but the lines endpoint's filter set omits payment_method (order-level attribute) and BaseFilter silently skips unknown AND-filter fields — so selecting a payment method returned unfiltered lines. Gate the control to the orders view (mirrors search being view-specific) and strip paymentMethod in buildPurchaseLinesQuery so a value carried over from the orders view can't emit a dead clause. Orders view is unchanged (buildPurchaseQuery still emits payment_method). - Reports layout had no catch-all route, so an unknown /reports/... path rendered only the breadcrumb shell. Add <Route component={NoMatchPage} /> last in the Switch, matching sponsor-layout/event-layout. Eager import (not lazy) to match this layout's own convention — it eager-imports its pages and has no Suspense. - Fold usePivot into its only consumer (sponsor-asset-report-page) and delete the hook. A single-export helper with one consumer belongs in the consumer, not the shared src/hooks dir (react-frontend.md §2). usePrint stays in src/hooks — it's generic (window.print) and used by all three report pages. Full suite green (131 suites / 1131 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * test(sponsor-reports): regression tests for the summit-gate rebuttal Encodes the invariant behind the "mount-only effect never re-runs on summit change → stuck empty" review concern, so a future refactor that breaks it fails CI instead of silently reintroducing the bug. - sponsor-reports-actions: mount fetches (getSponsorAssetFilters/Rows, getPurchaseDetailsFilters) no-op with no current summit, across null / {} / { id: 0 } (a weaker `!currentSummit` guard fails the {} and { id: 0 } cases). - summit-id-layout: the gate renders nothing unless currentSummit.id && summitId === currentSummit.id && !loading && hasLoaded, with each negative case isolating ONE clause so deleting that clause fails exactly its test. - sponsor-asset-report-page: with the real thunks (only getRequest mocked), a fresh mount fetches the current summit and a remount fetches the NEW summit — the direct observation a SummitIdLayout-driven remount produces on a switch. Tests only; no source change. Full suite green (137 suites / 1164 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): guarantee ContentCell img alt is a string (smarcet review) Verified real (not a false positive) by smarcet: alt={row.module?.title} renders an <img> with NO alt attribute when module/title is missing — worse than alt="" for screen readers (they announce the filename/URL). module can be undefined on this row shape (PivotTree's Leaf defends against exactly that). - alt now falls back to row.module?.title || row.module?.component_name || "" (mirrors PivotTree's component_name || title idiom; "" marks it decorative). - ContentCell.test.js: cover the previously-untested image branch with module absent — assert alt="" is present, not an omitted attribute. Also renames a shadowing `thunk` it.each param (no-shadow) in the guard tests. Full suite green (137 suites / 1165 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * refactor(sponsor-reports): address PR #997 review (reducer state, uicore dedup, paging, test prune) Reducer-driven list state (#1): - Move pagination/sort/filter out of component useState into the purchase-details, lines, and sponsor-asset reducers; thunks record them via getRequest's 5th-arg REQUEST payload and components read from props. Mirrors ticket-type-list-page. Adopt uicore components, drop duplicates (#2-#4 + parent sweep): - MuiDropdown for the status/form/payment selects, sponsor-asset status, and PivotSelector; uicore SearchInput in FilterBar; uicore DateTimePicker for the date filters (values kept as YYYY-MM-DD via a UTC boundary conversion, query contract unchanged). - SummaryPanel -> uicore InlineCard; StatusRollupChips -> mui/chip-list. Use the global loading overlay (#5): - Remove per-slice `loading` from all four reports reducers; thunks bracket start/stopLoading so state.baseState.loading + the single AjaxLoader own it. Sponsor-asset keeps a local hasFetched flag for first-mount empty-state suppression. Fold single-consumer helper (#6): - Inline bucketLinesBySponsor into LinesManifestView; delete manifest-grouping.js. Bulk-load sponsor-asset rows with bounded concurrency (#10 / smarcet): - Replace the per_page=5000 fetch (removed ALL_ROWS_PER_PAGE) with page-1-then-parallel paging at per_page=100 via p-limit + Promise.all, matching ticket-actions. Re-check the sequence token inside the pool callback so a superseded invocation's queued jobs cannot fire an identical-page request that would abort the fresh invocation's (under getRequest's page-bearing abort key). Follow-up correctness fixes (Codex review): - toReportDate treats the picker's moment(0) clear-sentinel as "no date" (matches the .unix()||null filter convention) so clearing a date drops the filter instead of sending 1970-01-01. - FilterBar search applies live (debounced) instead of feeding the Apply-gated draft, so a fast type+Apply can't submit a stale value. - Generation-guard hasFetched so a superseded fetch can't flash the empty state. Prune tests to meaningful coverage (parent + #7-#9): - Drop tests that exercised third-party libraries or were low-value (summit-switch, ReportShell, reducer-loading, TierBadge neutral, the manifest-grouping unit test) and reduce sponsor-reports suites to the behavior we own. Add mutation-proof regression tests for the fixes above. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): review follow-ups (lines page size, FilterBar default, dead code) Whole-branch review findings that survived verification: - Restore the Line Items (manifest) default page size to 50. The reducer relocation had set the lines DEFAULT_STATE.perPage to DEFAULT_PER_PAGE (10); the page test seeded perPage:50 in its fixture and masked the wrong default, so the live first fetch pulled 10. Use FIFTY_PER_PAGE + add a reducer test asserting the default (the test that would have caught the regression). - Default FilterBar `value` to a module-level stable {} (EMPTY_VALUE) instead of an inline {}, so omitting the prop no longer makes the value-resync effect loop on a fresh object each render. - Remove dead `query: {}` from the purchase-details reducer DEFAULT_STATE (never read or written). - Remove unused ERROR_CODE_503 constant (503 is handled generically). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): dropdown label overlap + empty Type filter (browser QA) Two issues found while testing the filters in the browser: - MuiDropdown label overlapped the value. The uicore component hardcodes displayEmpty and renders the placeholder (or "Select an option...") in the value area, while a `label` prop becomes a separate InputLabel that never shrinks — so the two overlap. Drive it by `placeholder` (the field name) instead, and restore the combobox's accessible name via SelectDisplayProps={{ "aria-label" }} (a bare aria-label on Select doesn't reach the role="combobox" element). Applies to the Purchase Status / Type / Payment Method selects and the Sponsor Assets status select. - Type filter showed only "Any". The filters endpoint returns forms as { code, name } where name is the (often blank) form_type; the old code dropped every blank-name form, while the table still shows them via each row's prebuilt form.display ("{code} - {name}"). Fall back to the code as the label when the name is blank (dropping only entries with no code), so the Type filter lists the types and matches the table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): translate asset status pills (Copilot review) The Sponsor Assets pivot tree and drill-down rendered the raw status token ("completed", "in_progress") as the StatusPill label, while the same report's tiles and filter use the translated sponsor_reports_page.status_* labels. Translate the label at both asset call sites, with a notFound fallback to the raw token for any status without a key (Media-scoped report only yields the three keyed statuses). Purchase-side pills (OrdersTable, Lines) keep the raw status, which is already human-readable and has no status_* keys. Scope the by_status tiles test to rows:[] so the tile labels don't collide with a seeded row's now-translated pill under the key-echo i18n mock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * refactor(sponsor-reports): address santi approval nits (createAction, tone map, STATUS_KEYS) Three of the five nits from the approving review (the seq-guard one is @smarcet's platform-level call; the FilterBar EMPTY_VALUE one is pushed back on — an inline {} default re-fires the value-resync effect every render): - clearPurchaseDetailsValidation now uses createAction via a thunk, matching the house idiom (summit-actions clearCurrentSummit et al) instead of a hand-rolled plain action object. - Sponsor Assets tiles derive their tone from StatusPill's exported statusTone() instead of a duplicated local TILE_TONE map. - Hoist the displayed-status list to a single exported STATUS_KEYS in build-pivot-tree (which computes the rollup those keys describe); the rollup zero-init, StatusRollupChips, and the asset page's tiles + status filter all consume it instead of holding literal copies. - FilterBar live-search test now OMITS `value`, exercising the module-level EMPTY_VALUE default; regressing it to an inline {} default loops the resync effect ("Maximum update depth exceeded") and blows the test up — mutation-verified against that exact change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF * fix(sponsor-reports): sequence-guard report thunks against stale responses Port the stale-response guard from getSponsorAssetRows to the sibling thunks smarcet flagged (getPurchaseDetailsReport, getPurchaseDetailsLinesReport, getSponsorAssetSponsor): concurrent invocations with different filters, page, sort, or sponsorId carry different getRequest abort keys (only access_token is stripped), so they never cancel each other and whichever response lands last would win, even the older one. Reachable via the pre-startLoading token-refresh window or an A-to-B drill-down navigation, where sponsor A's late response could render under sponsor B's URL. Extract the counter boilerplate into a shared sequenced() factory: each invocation begins a sequence before the token await and gets isCurrent() plus guardedDispatch(), which drops every dispatch (REQUEST, RECEIVE, error handlers, start/stopLoading) once a newer invocation has begun. The newest invocation is always current, so the overlay is always cleared by whichever call finishes last. A stale call's 401 re-login is dropped too (uicore authErrorHandler reaches doLogin by dispatching a thunk through the dispatch it is given) — safe: the fresh call carries the same token, 401s too, and drives the re-login. getSponsorAssetRows is refactored onto the same helper (behavior unchanged; its three existing stale tests pass as-is). Also guard the two filters thunks: safe for same-summit duplicates (identical abort key, getRequest cancels), but a summit switch changes the URL, so the old summit's late filters response would land after the SET_CURRENT_SUMMIT reset and repopulate filterOptions with the wrong summit's options. Add a stale-response test per newly guarded thunk (including the sponsor A over B drill-down and the summit-switch filters case), each mutation-verified: un-guarding the thunk makes its test fail with the stale response landing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbjUV2hmmvm8BWErNvJfdF --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.