MPDX-9834 Add year filter to MPGA#1929
Conversation
|
Preview branch generated at https://mpdx-9834-add-year-filter-on-mpga.d3dytjb8adxkk5.amplifyapp.com |
Bundle sizes [mpdx-react]Compared against ea57016
|
kegrimes
left a comment
There was a problem hiding this comment.
🤖 Multi-Agent Code Review — ✅ APPROVED WITH SUGGESTIONS
PR #1929 — Add year filter to MPGA · Mode: standard · 6 specialized agents + dependency analysis + 2 gap-review agents + 3 adversarial verifiers.
A well-executed refactor. It deletes TotalsContext and a large amount of prop-drilling, consolidates fetch/derive/totals into one ReportContext, and the new year/YTD date-window + zero-fill logic is verified correct (no off-by-one in firstFutureMonthIndex, totals not inflated by future zeros, date windows serialize correctly, no broken imports). No blockers. All findings below are improvements.
Risk Assessment
10/10 · CRITICAL — driven by breadth (17 medium-risk feature files) + change volume + the financial-reporting domain, not by any inherently dangerous infra file. Recommended reviewer level: SENIOR (financial display logic).
Dependency Impact
ReportContext.tsx has 15 dependents, all updated in this PR (self-contained). Every breaking edit — deleted TotalsContext, useGetLastTwelveMonths(locale, year?) signature change, hideDateRange→isMpgaReport rename, un-exported getFiltersWithCalculatedDates — has its call sites fixed in-PR. External consumer StaffExpenseReport (not in PR) verified safe.
Findings not anchored to a diff line (coverage gaps, related files, pre-existing)
Medium (5.0–6.9)
🟠 [6.5] Positional month↔label alignment is fragile — src/components/Reports/MPGAIncomeExpensesReport/Helper/filterFunds.ts:92,135,187 (Data Integrity, PLAUSIBLE; related file, not in PR)
Row monthly[] is built by array index from breakdownByMonth; monthLabels and the zero-fill are indexed independently — nothing joins on month.month. Now that arbitrary years / partial YTD windows are queryable, a sparse or reordered breakdownByMonth from the server would silently shift every column (April's amount under March's header; a real month grayed as "future"). Fix: key months by month.month, or assert the one-ordered-entry-per-requested-month server contract.
🟠 [6.0] Deleted negative-net calc test not migrated — Charts/MonthlySummaryChart test (diff 374-407) (Testing)
The removed test was the only assertion covering a negative monthly net (expenses > income). Re-add an equivalent net-calc assertion driven through the context mock.
🟡 [5.0] Headline "graying" feature has no rendering test — Tables/{TableCard,PrintTables,TotalRow} (Testing, CONFIRMED; downgraded from 7.5 by adversarial verify)
The derivation is unit-tested in ReportContext.test.tsx, but no component test activates YTD, so the future-month classes / gradient underline / colSpan-split paths never execute under test. MPGAIncomeExpensesReportTestWrapper can't seed filters — closing this needs an initialFilters seam on ReportProvider/the wrapper. Real regression risk, not a live bug.
Suggestions (<5.0)
- [4.0] Empty-state copy hardcodes "in the last 12 months" —
Tables/PrintTables.tsx:259-260,DisplayModes/ScreenOnlyReport.tsx:42,57(UX, in-scope but pre-existing lines). Now contradicts the dynamicsubtitlewhen a specific year / YTD is selected and yields no data. Threadsubtitle(already onuseReport()) into the empty-state copy. - [3.0] Average column mixes two divisor conventions in YTD — server
averagePerMonth(plain/subcategory rows) vs clienttotal / monthly.length(split/combined rows). Verified the client divisor uses the elapsed-month array N, so they agree if the server also divides by N; normalize to one convention. No demonstrable miscalc today.
Pre-existing (informational, not counted toward verdict)
- Hardcoded
currency = 'USD'also inBreakdownModal.tsx:41(not migrated touseReport()like its siblings). NocurrencyCodeis read from the data → wrong symbol for non-USD accounts. Worth a follow-up ticket given the report's purpose. SettingsDialog<Dialog>missingaria-labelledby→DialogTitle; empty-statecolSpan={15}magic number inPrintTables; Cancel buttonsx={{ color: 'black' }}(hardcoded, use a theme token).
Agent Summary
| Agent | Blockers | Medium | Suggestions | Confidence |
|---|---|---|---|---|
| Security | (skipped — no auth/api/env/apollo-link files) | |||
| Architecture | 0 | 0 | 5 | Med-High |
| Data Integrity | 0 | 1 | 2 | High |
| Testing | 0 | 2 | 3 | High |
| UX | 0 | 1 | 4 | High |
| Standards | 0 | 1 | 0 | High |
| Financial Reporting | 0 | 0 | 2 | Med-High |
Financial Checklist: Money arithmetic safe ✅ · Currency mixing prevented ✅ (single server-aggregated figure) · Rounding at display boundary ✅ · Null/undefined handled ✅ · Luxon (not new Date()) ✅ · Server aggregations preferred ✅ (caveat: Average mixes server+client — see above)
Inline comments below carry a
<!-- severity:X.X -->tag. Reply/dismiss: <reason>on any finding with severity < 7 you disagree with, then re-run the review.
🤖 Generated with Claude Code
|
@zweatshirt I realized Will was out! So sorry for the large PR🫣, but most of it is removing prop drilling. I noticed it was getting bad when I was adding the filters. Can you just make sure nothing is broken with the filters and the code (unrelated to context) looks decent? |
zweatshirt
left a comment
There was a problem hiding this comment.
Tried to comment on whatever I was able to find. The UI looks amazing and seems to behave as expected so I am going to approve. Just had some suggestions.
| const incomeData = data.income ?? []; | ||
| const expenseData = data.expenses ?? []; |
There was a problem hiding this comment.
Optional: We can use de-structuring here
| </GqlMockedProvider> | ||
| </LocalizationProvider> | ||
| </ThemeProvider>, | ||
| it('displays the tables that should be showing', async () => { |
There was a problem hiding this comment.
This test checks that there are 2 tables but only seem to add an assertion for one table by text. What is the second table that is supposed to display? Can we add an assertion for it in this test?
| incomeTotal, | ||
| expensesTotal, | ||
| ministryTotal, | ||
| healthcareTotal, | ||
| assessmentTotal, | ||
| benefitsTotal, | ||
| salaryTotal, | ||
| otherTotal, |
There was a problem hiding this comment.
I would group these, something like this:
totals: {
income, expenses, ministry, healthcare,
assessment, benefits, salary, other,
}
| .slice(0, index) | ||
| .reduce((sum, group) => sum + group.count, 0); | ||
|
|
||
| const futureStart = |
There was a problem hiding this comment.
What do you think of changing this to something like pastCount instead of futureStart?
|
|
||
| const futureStart = | ||
| firstFutureMonthIndex !== undefined | ||
| ? Math.max( |
There was a problem hiding this comment.
Can futureStart ever go negative? Wondering if this Math.max is needed or not
| sx={{ | ||
| '& .future-month-header .MuiDataGrid-columnHeaderTitle': { | ||
| color: theme.palette.text.disabled, | ||
| }, | ||
| '& .future-month': { | ||
| backgroundColor: theme.palette.action.hover, | ||
| }, | ||
| }} |
There was a problem hiding this comment.
Optional:
| sx={{ | |
| '& .future-month-header .MuiDataGrid-columnHeaderTitle': { | |
| color: theme.palette.text.disabled, | |
| }, | |
| '& .future-month': { | |
| backgroundColor: theme.palette.action.hover, | |
| }, | |
| }} | |
| sx={{ | |
| '.future-month-header .MuiDataGrid-columnHeaderTitle': { | |
| color: theme.palette.text.disabled, | |
| }, | |
| '.future-month': { | |
| backgroundColor: theme.palette.action.hover, | |
| }, | |
| }} |
| }, | ||
| ]; | ||
| }, [monthCount, getBorderColor, t]); | ||
| }, [monthCount, getBorderColor, firstFutureMonthIndex, t]); |
There was a problem hiding this comment.
Probably won't ever trigger but theme could go in the dependency array
| const isFilterActive = useMemo( | ||
| () => | ||
| Boolean( | ||
| filters && | ||
| (filters.selectedDateRange === DateRange.YearToDate || | ||
| (filters.selectedYear !== null && | ||
| filters.selectedYear !== undefined)), | ||
| ), | ||
| [filters], | ||
| ); |
There was a problem hiding this comment.
We can probably keep this un-memoized
| const hasActiveFilter = Boolean( | ||
| newFilters && | ||
| (newFilters.categories || | ||
| newFilters.selectedDateRange === DateRange.YearToDate || | ||
| (newFilters.selectedYear !== null && | ||
| newFilters.selectedYear !== undefined)), | ||
| ); |
There was a problem hiding this comment.
This and isFilterActive are nearly identical, what do you think about creating a shared variable for both conditions?
| const transformedData: Funds[] = useMemo( | ||
| () => | ||
| (reportData?.reportsStaffExpenses?.funds ?? []).map((fund) => ({ | ||
| ...fund, | ||
| categories: (fund.categories ?? []).map((category) => ({ | ||
| ...category, | ||
| category: category.category, | ||
| breakdownByMonth: category.breakdownByMonth.map((month) => ({ | ||
| ...month, | ||
| })), | ||
| subcategories: (category.subcategories ?? []).map((subcategory) => ({ | ||
| ...subcategory, | ||
| subCategory: subcategory.subCategory, | ||
| breakdownByMonth: subcategory.breakdownByMonth.map((month) => ({ | ||
| ...month, | ||
| transactions: (month.transactions ?? []).map((transaction) => ({ | ||
| transactedAt: transaction.transactedAt, | ||
| description: transaction.description ?? '', | ||
| amount: transaction.amount, | ||
| })), | ||
| })), | ||
| })), | ||
| })), | ||
| })), | ||
| [reportData], | ||
| ); |
There was a problem hiding this comment.
Wondering if it is worth adding a comment for this function explaining why we transform the data here
Description
MPGA UAT issues:
Extra:
Jira ticket: MPDX-9834
Testing
Impersonate: courtney.campbell@cru.org
No filter (default) test:
/reports/mpgaIncomeExpensesYear to Date filter test:
/reports/mpgaIncomeExpensesPast year filter test:
/reports/mpgaIncomeExpensesChecklist:
/pr-reviewcommand locally and fixed any relevant suggestions