Skip to content

Available Funds source rows act as per-source filter; Cost/Payback/Net per source #1356

@steilerDev

Description

@steilerDev

As a homeowner reviewing the Cost Breakdown overview,
I want the Available Funds source rows themselves to act as the per-source filter (with all sources selected by default and Cost / Payback / Net columns aggregated per source),
so that I can see the live impact of including or excluding each fund without a parallel chip toolbar, and so that hidden sources visibly remove their cost contributions from the breakdown and Available Funds total.

Context

This story supersedes the chip-toolbar UX shipped in #1354 / PR #1355. After using that implementation, the redundancy of having both source detail rows and a chip strip (and the static Available Funds total even while filtering) was confirmed to be confusing. This story converts the source rows into the filter affordance and makes Available Funds responsive to the filter.

Parent Epic: none (standalone enhancement; no budget epic currently open)
Priority: Should Have
Supersedes: #1354, PR #1355

Acceptance Criteria

A. Filter affordance lives on the source rows (chip strip removed)

  • AC-1: When the Available Funds row is expanded, the chip strip / BudgetSourceChip toolbar is not rendered. There is no separate "all sources" clear button above the source rows.
  • AC-2: Each source detail row (and the synthetic "Unassigned" row, when unassigned lines exist) is itself the toggle control. Clicking the row toggles the source's selection state.
  • AC-3: The toggle control is keyboard-operable: each source row is reachable via Tab and toggles on Enter or Space. Each row exposes aria-pressed (or equivalent) reflecting the selected state, with an accessible name that includes the source name and selection state.
  • AC-4: A screen-reader live region announces the number of currently selected sources (e.g., "3 of 4 sources selected") whenever selection changes.

B. Default state and visual deselection

  • AC-5: On initial page load with no filter URL parameter, all sources (including the Unassigned bucket, if present) are selected. No source rows render in the deselected/greyed style.
  • AC-6: When a source is deselected, the row renders in a greyed-out / disabled visual treatment per the UX spec (reduced opacity or muted token). The source dot color is preserved at reduced emphasis to keep the source identifiable.
  • AC-7: Deselecting all sources is permitted; in that case the breakdown's Sum, Available Funds total, and all areas/items render the empty state already used by the breakdown table.

C. Cost / Payback / Net columns per source row

  • AC-8: The Cost column on a source detail row shows the perspective-resolved sum of cost across all budget lines linked to that source (across both Work Items and Household Items), formatted with a leading minus sign consistent with the rest of the cost column. Lines covered by a non-quotation invoice contribute their actual cost; quotation invoices contribute a ±5% range resolved at the active perspective; other lines contribute the confidence-margin range resolved at the active perspective.
  • AC-9: The Payback column on a source detail row shows the perspective-resolved sum of subsidy payback attributable to that source's budget lines (see "Architectural questions" below — exact attribution rule must be confirmed by product-architect before implementation). Renders as when there is no payback for the source.
  • AC-10: The Net column on a source detail row shows totalAmount − cost + payback for that source, i.e., remaining funds available for that source given current commitments. The value is colored positive/negative consistent with the existing Net column treatment.
  • AC-11: Cost / Payback / Net values on each source row are independent of the source row's selection state — they always reflect that source's full attribution. (Selection toggles cascade only into the filtered breakdown above and the Available Funds total below.)

D. Filter cascade (rows above source rows)

  • AC-12: When one or more sources are deselected, budget line rows whose budgetSourceId (or "unassigned" for null) is not in the selected set are excluded from the breakdown rendering.
  • AC-13: When a Work Item, Household Item, or Area has zero matching budget lines under the current filter, the entire item or area row is hidden — not rendered as an empty row with zero values.
  • AC-14: Work Item / Household Item / Area subtotal rows that remain visible recompute Cost, Payback, and Net to reflect only the lines that pass the filter (consistent with the perspective resolution rules already used).
  • AC-15: The Sum row at the top of the summary section recomputes Cost, Payback, and Net from the filtered set of lines. The Remaining Budget row recomputes accordingly.

E. Available Funds total responds to the filter

  • AC-16: The Available Funds value on the Available Funds row is the sum of totalAmount across currently selected sources only. With all sources selected, this matches the unfiltered Available Funds figure shown today.
  • AC-17: When all sources are deselected, the Available Funds row shows 0 and the Remaining Budget row recomputes against the (zero) filtered Available Funds.
  • AC-18: The Available Funds metric in the Budget Health hero card (top of the page) is out of scope for this story and continues to display the unfiltered total. If a follow-up reveals the hero card should also respond, that is a separate story.

F. URL state and persistence

  • AC-19: The default state — all sources selected — produces no ?sources= URL parameter (clean URL on page load and after a "select all" interaction).
  • AC-20: A non-default selection is encoded in the URL using a clearly-named parameter (recommended: ?deselectedSources=<comma-separated-ids>). The exact parameter name and convention is finalised by product-architect; the chosen convention must be documented in the implementation spec.
  • AC-21: Reloading the page with a deselected-sources URL restores the same selection state.
  • AC-22: Sharing or bookmarking a URL with deselected sources reproduces the same filter on another browser session.

G. Cleanup

  • AC-23: The BudgetSourceChip component, its CSS module, its tests, and its i18n keys are removed if no other consumer remains. If another page consumes it, this fact must be documented in the implementation spec.
  • AC-24: The Escape-to-clear keyboard handler tied to the chip toolbar (handleToolbarKeyDown in CostBreakdownTable.tsx) is removed or replaced by an equivalent affordance discovered by ux-designer (e.g., a "select all" action somewhere else). Behavior must be documented in the visual spec.
  • AC-25: All E2E and unit tests that previously asserted on the chip toolbar are updated or removed; new tests cover the row-toggle interaction, the per-source Cost/Payback/Net columns, the area/item hiding rule, and the Available Funds re-summation.

UAT Scenarios (Given/When/Then)

UAT-1: Default state shows all sources selected

  • Given a project with three budget sources (S1, S2, S3) and assigned budget lines under each
  • When I open /budget/overview and expand the Available Funds row
  • Then I see three source detail rows, none rendered in the greyed/disabled style
  • And there is no chip toolbar above the source rows
  • And the URL contains no ?sources= (or ?deselectedSources=) parameter
  • And the Available Funds value equals S1.totalAmount + S2.totalAmount + S3.totalAmount

UAT-2: Deselecting a source greys it out and excludes its lines

  • Given the default state above and S1 has lines linked to Work Item WI-A
  • When I click the S1 source row
  • Then the S1 row renders in the greyed/disabled style
  • And aria-pressed on the S1 row is false
  • And budget lines whose source is S1 are no longer rendered
  • And if WI-A has no other selected-source lines, WI-A is no longer rendered
  • And the Sum row Cost decreases by the S1-attributed cost
  • And the Available Funds row shows S2.totalAmount + S3.totalAmount
  • And the URL updates to encode S1 as deselected

UAT-3: Deselecting all sources shows empty state

  • Given the default state with S1, S2, S3 selected
  • When I deselect S1, S2, S3 in turn
  • Then no work items, household items, or areas are rendered in the breakdown body
  • And the empty state already used by the breakdown table appears
  • And the Available Funds row shows 0
  • And the source rows themselves remain visible (so the user can re-select them) and all are greyed

UAT-4: Per-source Cost/Payback/Net columns

  • Given S1 has total amount 100,000, lines summing to 40,000 of perspective-resolved cost, and 5,000 of payback attributed to S1's lines
  • When I view the S1 source row at any selection state
  • Then the Cost cell shows -€40,000 (or locale equivalent)
  • And the Payback cell shows €5,000
  • And the Net cell shows €65,000 colored positive
  • And the values do not change when I toggle S1's selection

UAT-5: URL persistence

  • Given S1 deselected, S2 and S3 selected
  • When I copy the URL, open it in another tab/session
  • Then S1 is rendered greyed and S2/S3 are selected
  • And the breakdown body, source-row Cost/Payback/Net, and Available Funds total match those of the original tab

UAT-6: Keyboard operation

  • Given a focused source row
  • When I press Enter or Space
  • Then the row's selection toggles
  • And the screen-reader live region announces the new "X of N sources selected" count

Out of Scope

  • Source CRUD (create/edit/delete sources) — handled in /budget/sources
  • Subsidy CRUD or attribution logic on subsidies themselves
  • Changes to the Subsidy Adjustments section of the breakdown table (the "oversubscribed" rows below the cost section)
  • Changes to the Budget Health hero card (Available Funds, Projected Cost Range, Expected Payback, Remaining metrics)
  • Changes to the bar chart segments at the top of the page
  • Changes to invoices, vendors, or budget line CRUD pages
  • Re-shaping the breakdown API contract to push filter logic to the server. Filtering remains client-side over the existing GET /api/budget/breakdown payload unless product-architect determines the per-source payback aggregate (AC-9) requires a server-side field — in which case a contract addendum is in scope of this story.

Architectural questions for product-architect

These must be resolved before implementation. The dev-team-lead spec must reference the resolution and the wiki must be updated if any new convention lands.

  1. Per-source payback attribution (AC-9 / AC-10). The current breakdown service computes subsidy payback per entity (work item or household item), not per budget source. The subsidy engine consumes all of an entity's budget lines and emits a single payback figure. There is no source attribution in the model today. Options:

    • (a) Pro-rata at entity level: split each entity's payback across the entity's lines weighted by plannedAmount, then sum per source. Simple, deterministic, but ignores subsidy-category specificity.
    • (b) Re-run engine per source slice: re-call computeSubsidyEffects against only the subset of an entity's lines linked to a given source. Keeps the engine's category-matching logic honest but is O(entities × sources) and may not preserve the global subsidy cap behavior.
    • (c) New server-side per-source aggregate: extend BudgetSourceSummaryBreakdown with paybackMin/paybackMax computed during the existing per-entity pass, with the attribution rule defined by the architect.
    • Architect to pick (a/b/c) or propose another option, document the rule, and update BudgetSourceSummaryBreakdown and/or the breakdown service accordingly. The story's AC-9 should be considered conditional on this decision.
  2. URL-state semantics (AC-19, AC-20). Today the ?sources= parameter encodes selected sources, with empty meaning "all selected" (an inverted convention that is fragile when source IDs change). The story recommends flipping to ?deselectedSources=<ids> so default = clean URL. Architect to confirm the parameter name and any backwards-compatibility for existing bookmarks of the chip-era URL.

  3. Subsidy-cap interaction with filtering (AC-15). When sources are deselected, subsidy payback may exceed a subsidy's cap less aggressively than the unfiltered view. Should the Subsidy Adjustments section recompute under the filter, or stay anchored to the full project view? Recommendation: stay anchored (it's a project-wide concept), but architect to confirm.

Notes

  • This is a UX rework of an already-shipped feature; expect both code removal (BudgetSourceChip, the chip toolbar block in CostBreakdownTable.tsx lines ~1265-1306, the handleToolbarKeyDown handler and the availFundsButtonRef) and additions (row-toggle, per-source payback, Available Funds re-summation).
  • ux-designer must produce a visual spec for the row-as-toggle pattern: greyed style tokens, focus ring, hover treatment, dark-mode parity, and any "select all" action that may replace the Escape-to-clear handler.
  • qa-integration-tester owns unit + integration tests; e2e-test-engineer owns Playwright tests for UAT-1 through UAT-6 across desktop, tablet, and mobile viewports.

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions