Skip to content

chore(deposits): dedupe split helper, extract OverflowMenu, fix silent revert errors #1413

@steilerDev

Description

@steilerDev

Summary

Post-#1404/#1405 cleanup arising from product-architect's reviews of PR #1407 (comment) and PR #1412 (comment). Bundles four medium-severity recommendations from the same review cycle:

  1. Deduplicate the proportional deposit-split formula now inlined in four service files (refactor, no behaviour change).
  2. Extract a shared OverflowMenu component from the 1363-line InvoiceDepositsSection.tsx (refactor, no behaviour change).
  3. Replace an inline style={{}} JSX prop with a CSS-Module class (refactor, no behaviour change).
  4. Surface API errors in the three deposit-revert handlers that currently swallow failures silently (bug fix, user-visible UX improvement).

Items 1-3 are behaviour-preserving; item 4 is a bug fix. Combined into one issue because all four are closely related cleanup from the same architect review cycle.

Findings

1. Extract splitByDeposits() helper to dedupe the proportional-split pattern

The proportional deposit-split formula is now inlined in four places:

All four share a common shape:

  • Group rows by invoice_id
  • For each invoice, compute residual = max(0, invoice.amount - sum(deposits.amount))
  • Bucket the residual under the parent invoice's status
  • Bucket each deposit under its own status

Extract a single splitByDeposits() helper that returns the per-invoice split as a structured value, then have the four call sites reduce over those splits to produce their respective aggregate shapes.

2. Extract shared OverflowMenu component from InvoiceDepositsSection.tsx

client/src/pages/InvoiceDepositPage/InvoiceDepositsSection.tsx is ~1363 lines, and the per-row overflow menu (trigger button + menu items + keyboard navigation + focus management) is duplicated almost verbatim between DepositRow (desktop table) and DepositCard (mobile cards) — ~330 lines duplicated.

Extract a single shared OverflowMenu component to client/src/components/OverflowMenu/ per the project's "Component Reuse Policy" (per CLAUDE.md: "every new component must be built as a reusable shared component — no one-off implementations"). The component should expose:

  • items: Array<{ label: string; onClick: () => void; ... }> (or similar) for the menu items
  • triggerAriaLabel: string for accessibility
  • Full WAI-ARIA Menu Button keyboard nav (ArrowUp/Down/Home/End/Escape, focus return on close) — same behaviour as the inline implementation today

Important

Per project policy, every new shared component requires a ux-designer visual spec before implementation begins. The orchestrator must launch the ux-designer agent for the visual spec phase prior to launching the frontend-developer.

3. Replace inline style={{}} on table row with CSS Module class

InvoiceDepositsSection.tsx contains an inline style={{ ... }} on a table row (architect flagged it as the same anti-pattern as the recent fd73bcad fix on the budget-line form). Move the inline declaration to a named class in InvoiceDepositsSection.module.css.

4. Surface API errors in revert handlers

In InvoiceDepositsSection.tsx, the handleRevertToPending, handleRevertToPaid, and handleStateConfirm functions catch API errors silently — the user gets no feedback when a transition fails (e.g., on INVALID_DEPOSIT_STATUS_TRANSITION or network errors). The PR-review by product-owner and architect both flagged this as a UX gap.

Surface the error to the user via the existing toast/banner pattern or via FormError in the relevant modal. For revert actions (which fire PATCH immediately without a modal), surface a transient error banner or toast at the section level. Use the existing translateApiError(code, tErrors) helper for code mapping.

Acceptance Criteria

  • AC-1 A shared splitByDeposits(rows) function exists in server/src/services/shared/depositAggregateUtils.ts and is reused by computeDepositAwareAggregates, computeStatusContribution, aggregateInvoiceStatusBreakdown, AND computeDiscretionaryInvoiceAmount in budgetSourceService.ts. Behaviour preserved exactly — existing tests still pass without modification.
  • AC-2 A reusable OverflowMenu shared component exists under client/src/components/OverflowMenu/. DepositRow and DepositCard in InvoiceDepositsSection.tsx both consume it instead of duplicating the menu code. Full WAI-ARIA Menu Button keyboard nav preserved (ArrowUp/Down/Home/End/Escape, focus return on close). Existing E2E tests for the deposit menu pass without modification.
  • AC-3 No style={{}} JSX prop remains in InvoiceDepositsSection.tsx — moved to InvoiceDepositsSection.module.css with named classes.
  • AC-4 The three revert handlers in InvoiceDepositsSection.tsx (handleRevertToPending, handleRevertToPaid, handleStateConfirm) surface API errors to the user. Server error codes are translated via translateApiError(code, tErrors). Network errors show a generic translated message. Use the existing FormError shared component for modal-based actions and an inline transient banner for the menu-driven Revert actions (which don't open a modal). Add at least one E2E test that verifies the error is visible after a forced failure (or, if E2E setup is too heavy, an integration/component test that simulates the API rejection).
  • AC-5 All existing tests pass without modification (refactor is behaviour-preserving for AC-1 through AC-3). New tests added for AC-4 (error visibility).
  • AC-6 No new dependencies, no schema changes, no API contract changes.
  • AC-7 UX-designer approves the visual spec for the new OverflowMenu shared component before implementation begins.

Non-Goals

  • No UX behaviour changes for items 1-3 (pure refactors).
  • No new deposit-related features.
  • The mobile bottom-sheet modal (separate UX issue, file separately if needed).
  • Other UX-designer nits (focus ring tokens, count chip tokens, statusQuotation dark mode, tablet column hiding) — file as separate refinement issues if desired; these are not part of "architect's recommendations."

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions