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)
B. Default state and visual deselection
C. Cost / Payback / Net columns per source row
D. Filter cascade (rows above source rows)
E. Available Funds total responds to the filter
F. URL state and persistence
G. Cleanup
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.
-
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.
-
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.
-
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.
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)
BudgetSourceChiptoolbar is not rendered. There is no separate "all sources" clear button above the source rows.aria-pressed(or equivalent) reflecting the selected state, with an accessible name that includes the source name and selection state.B. Default state and visual deselection
C. Cost / Payback / Net columns per source row
—when there is no payback for the source.totalAmount − cost + paybackfor 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.D. Filter cascade (rows above source rows)
budgetSourceId(or "unassigned" for null) is not in the selected set are excluded from the breakdown rendering.E. Available Funds total responds to the filter
totalAmountacross currently selected sources only. With all sources selected, this matches the unfiltered Available Funds figure shown today.0and the Remaining Budget row recomputes against the (zero) filtered Available Funds.F. URL state and persistence
?sources=URL parameter (clean URL on page load and after a "select all" interaction).?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.G. Cleanup
BudgetSourceChipcomponent, its CSS module, its tests, and itsi18nkeys are removed if no other consumer remains. If another page consumes it, this fact must be documented in the implementation spec.Escape-to-clearkeyboard handler tied to the chip toolbar (handleToolbarKeyDowninCostBreakdownTable.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.UAT Scenarios (Given/When/Then)
UAT-1: Default state shows all sources selected
/budget/overviewand expand the Available Funds row?sources=(or?deselectedSources=) parameterS1.totalAmount + S2.totalAmount + S3.totalAmountUAT-2: Deselecting a source greys it out and excludes its lines
aria-pressedon the S1 row isfalseS2.totalAmount + S3.totalAmountUAT-3: Deselecting all sources shows empty state
0UAT-4: Per-source Cost/Payback/Net columns
100,000, lines summing to40,000of perspective-resolved cost, and5,000of payback attributed to S1's lines-€40,000(or locale equivalent)€5,000€65,000colored positiveUAT-5: URL persistence
UAT-6: Keyboard operation
Out of Scope
/budget/sourcesGET /api/budget/breakdownpayload 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.
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:
plannedAmount, then sum per source. Simple, deterministic, but ignores subsidy-category specificity.computeSubsidyEffectsagainst 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.BudgetSourceSummaryBreakdownwithpaybackMin/paybackMaxcomputed during the existing per-entity pass, with the attribution rule defined by the architect.BudgetSourceSummaryBreakdownand/or the breakdown service accordingly. The story's AC-9 should be considered conditional on this decision.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.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
BudgetSourceChip, the chip toolbar block inCostBreakdownTable.tsxlines ~1265-1306, thehandleToolbarKeyDownhandler and theavailFundsButtonRef) and additions (row-toggle, per-source payback, Available Funds re-summation).