[MPDX-9369] - Address PR feedback for Hours Per Week calculator#1740
[MPDX-9369] - Address PR feedback for Hours Per Week calculator#1740
Conversation
Restructure act() calls to avoid returning values directly from act(), which expects void return. Capture return values via outer variables instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bundle sizes [mpdx-react]Compared against 685ea40 No significant changes found |
Moved the title to the page-level right panel header and removed the duplicate heading from HoursPerWeekGrid component. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Preview branch generated at https://MPDX-9369-PR-feedback.d3dytjb8adxkk5.amplifyapp.com |
wjames111
left a comment
There was a problem hiding this comment.
Multi-Agent Code Review Report
Agents: 6 specialized reviewers (Architecture, Testing, Standards, Data Integrity, UX, Financial Reporting) with cross-examination debate
Mode: Standard
Risk Score: 8/10 (HIGH)
Verdict: APPROVED WITH SUGGESTIONS
No blockers found. 5 medium-priority improvements recommended.
Summary
This is a well-structured refactoring PR that:
- Extracts ~300 lines of business logic from
HoursPerWeekGridinto a dedicateduseHoursPerWeekGridhook with comprehensive tests - Creates a reusable GraphQL fragment (
DesignationSupportHoursItemFields) eliminating field duplication across 3 operations - Makes the right panel title dynamic via context (
setRightPanel(title, content)API) - Correctly removes duplicate error snackbars (global Apollo error link already handles them)
Debate Resolution
The Data Integrity agent initially flagged a CRITICAL issue claiming saveHoursItem would fail for temp entries by sending temp- IDs to updateHoursItem. The Architecture agent disproved this by identifying the isCellEditable guard in HoursPerWeekGrid.tsx that explicitly blocks editing of temp- prefixed rows. The finding was withdrawn during debate.
Dependency Impact
useSaveField.ts behavioral change (error handling removed) affects ReimbursableExpensesGrid.tsx outside this PR — verify that component handles propagated errors.
Standards Checklist
| Standard | Status |
|---|---|
| Named exports only | ✅ |
| File naming conventions | ✅ |
GraphQL id fields |
✅ |
| Mutation cache updates | ✅ |
All strings use t() |
✅ |
No any types |
✅ |
| Colocated tests | ✅ |
| No debug output | ✅ |
| throw new Error(t('Failed to save hours entry.')); | ||
| } | ||
| }, | ||
| [calculation, createHoursItem, updateHoursItem, trackMutation, t], |
There was a problem hiding this comment.
createHoursItem is listed in the dependency array but never used in the saveHoursItem function body — only updateHoursItem is called. This is a leftover from the extraction refactor that causes unnecessary callback re-creation.
| [calculation, createHoursItem, updateHoursItem, trackMutation, t], | |
| [calculation, updateHoursItem, trackMutation, t], |
|
|
||
| await saveHoursItem(updatedEntry); | ||
|
|
||
| const updatedEntries = entries.map((entry) => |
There was a problem hiding this comment.
processRowUpdate reads entries from its closure to compute the new average, but updateEntry (line 222) already updated state via the functional setEntries updater. During rapid sequential edits, the entries used here would be stale.
Mitigated by DataGrid blocking behavior (cell edit waits for promise resolution) and refetchQueries re-sync, so unlikely in practice. But the average sent to server via saveField could be temporarily incorrect.
Consider computing the average inside a useEffect that watches entries changes, or using a ref for the latest entries.
| } catch { | ||
| setEntries((prev) => prev.filter((e) => e.id !== tempId)); | ||
| } | ||
| }, [t, calculation, createHoursItem, trackMutation, entries.length]); |
There was a problem hiding this comment.
addEntry computes newPosition via Math.max(...entries.map(e => e.position)) but only lists entries.length in the dependency array. If entries change content (e.g., positions) without changing length, the callback would use a stale closure.
| }, [t, calculation, createHoursItem, trackMutation, entries.length]); | |
| }, [t, calculation, createHoursItem, trackMutation, entries]); |
| const updatedEntry: HoursPerWeekEntry = { | ||
| id: newRow.id as string, | ||
| label: newRow.label as string, | ||
| hoursPerWeek: Number(newRow.hoursPerWeek) || 0, |
There was a problem hiding this comment.
weeks is properly floored with Math.max(0, clampedWeeks) on the next line, but hoursPerWeek has no equivalent guard. A negative hours value would corrupt the weighted average sent to the server.
Consider:
hoursPerWeek: Math.max(0, Number(newRow.hoursPerWeek) || 0),Or add min: 0 to the column definition in HoursPerWeekGrid.tsx.
| <Alert severity="warning" sx={{ mt: 1 }}> | ||
| {t('Weeks must add up to {{max}}. {{remaining}} week(s) remaining.', { | ||
| max: MAX_TOTAL_WEEKS, | ||
| max: 52, |
There was a problem hiding this comment.
MAX_TOTAL_WEEKS was moved to useHoursPerWeekGrid.ts but this line uses the raw number 52. Consider exporting the constant from the hook to prevent drift if the value ever changes.
zweatshirt
left a comment
There was a problem hiding this comment.
I'll try to finish up tomorrow. It looks good though
| const newPosition = | ||
| entries.length > 0 ? Math.max(...entries.map((e) => e.position)) + 1 : 0; |
There was a problem hiding this comment.
Could we try something like this: const newPosition = (entries.at(-1)?.position ?? -1) + 1;
It's minor, but it will make this O(1) instead of O(n)
| } | ||
| }, [t, calculation, createHoursItem, trackMutation, entries.length]); | ||
|
|
||
| const deleteEntry = useCallback( |
There was a problem hiding this comment.
In practice it probably doesn't matter as long as all new entries are greater than the highest position, but we aren't fixing the positions every time an entry is deleted. E.g. [1, 2, 3] -> delete at idx 1 and we will always be left with [1, 3] when it should probably be fixed to be [1, 2]. I don't think it matters too much though.
There was a problem hiding this comment.
Looks good Will! I think there some small places to do additional refactoring but but this is working well.
I only had one real concern I'm hoping we could fix before merging: When the averageHoursPerWeek is applied in the input field, it is never rounded. E.g. we get 2.0384615384615383 and not 2.04 displayed in the UI. We can either round just the display value, or if necessary, we can round what is sent to the server I guess (which I suggested previously we don't do that, but maybe it's not too critical)
I also noticed one small thing, if a user has already maxed out the number of weeks to 52, and returns to the calculator at a later time, the user can't input new # of Weeks, but has no alert as to why. This is minor.
| rows={dataWithTotal} | ||
| columns={columns} | ||
| processRowUpdate={processRowUpdate} | ||
| onCellEditStart={(params, event) => { |
There was a problem hiding this comment.
In the future I'm thinking we can change this to use BaseGrid's onCellEditStart={selectAllOnEditStart} behavior
| const newAverage = newTotalWeeks > 0 ? newTotalHours / newTotalWeeks : 0; | ||
|
|
||
| try { | ||
| if (!entryId.startsWith('temp-') && !entryId.startsWith('default-')) { |
There was a problem hiding this comment.
Do we need to still check for this 'default-' prefix?
| if (!entryId.startsWith('temp-') && !entryId.startsWith('default-')) { | |
| if (!entryId.startsWith('temp-')) { |
| }, [t, calculation, createHoursItem, trackMutation, entries.length]); | ||
|
|
||
| const deleteEntry = useCallback( | ||
| async (id: string | number) => { |
There was a problem hiding this comment.
What do you think of changing this to `HoursPerWeekEntry['id']? Not necessary, just curious
| numberOfWeeks: entry.weeks, | ||
| }, | ||
| }, | ||
| refetchQueries: ['PdsGoalCalculation'], |
There was a problem hiding this comment.
Claude note: refetchQueries: ['PdsGoalCalculation'] is likely wasted given the initializedRef one-time-load guard.
| [entries, deleteHoursItem, trackMutation, saveField], | ||
| ); | ||
|
|
||
| const processRowUpdate = useCallback( |
There was a problem hiding this comment.
Claude suggestion: Duplicated totals math across processRowUpdate / deleteEntry / the useMemos — candidate for a small helper.
Description
Follow-up PR addressing code review feedback from #1737.
useHoursPerWeekGridhook with dedicated tests (discussion)Testing
Checklist:
/pr-reviewcommand locally and fixed any relevant suggestions