Skip to content

Cost Breakdown Table: aggregated rows ignore source filter (subtotals and grand totals inconsistent) #1358

@steilerDev

Description

@steilerDev

Type: Bug (UX/correctness regression)
Priority: Should Have
Related: Follow-up to #1356 / PR #1357 (just merged)
Affected file: client/src/components/CostBreakdownTable/CostBreakdownTable.tsx

Problem

When the user deselects one or more sources via the per-source filter on the Available Funds rows, the Cost Breakdown Table is internally inconsistent: child rows (individual budget lines) are filtered, but most aggregated rows (work item subtotals, area subtotals at every depth, work-items / household-items section totals, and the payback portion of the Sum and Remaining Budget rows) continue to display project-wide values. This produces visible math mismatches — children no longer add up to their parents.

Expected behavior

When the source filter is active, every aggregated value in the cost section MUST recompute against the visible (filter-honoring) line set:

  • Cost columns at every level reflect the perspective-resolved cost of the visible budget lines underneath.
  • Payback columns at every level reflect only payback contributed by visible lines (the existing computePerSourcePayback pro-rata helper already returns per-source payback; aggregates can be summed across selected sources).
  • Net columns at every level reflect payback - cost using the filtered values above.
  • The Sum and Remaining Budget rows reflect only the filtered set in BOTH the cost AND payback terms.

Actual behavior — by row type

I read CostBreakdownTable.tsx end-to-end. Each <tr> rendered in the cost section, classified by filter-awareness:

# Row Code lines Currently sources from Filter-aware?
1 Section header "Work Items" (Level 0) 1132–1188 wiTotals.rawProjectedMin/Max, wiTotals.subsidyPayback, wiTotals.minSubsidyPayback NO — server-emitted project totals
2 Section header "Household Items" (Level 0) 1210–1266 hiTotals.* NO — server-emitted project totals
3 Work-item Area subtotal (Level 1, recursive at all depths) WorkItemAreaSection, 502–531 area.rawProjectedMin/Max, area.subsidyPayback, area.minSubsidyPayback NO — server-emitted area totals
4 Household-item Area subtotal (Level 1, recursive at all depths) HouseholdItemAreaSection, 711–740 area.* NO — server-emitted area totals
5 Work Item row (Level 2) WorkItemRow, 400–443 item.rawProjectedMin/Max, item.actualCost, item.subsidyPayback, item.minSubsidyPayback NO — server-emitted item totals. The row is hidden when ALL its lines are filtered (cascade), but when only SOME lines are filtered the row still displays the full project-wide item total.
6 Household Item row (Level 2) HouseholdItemRow, 606–651 item.* NO — same partial-filter problem as #5
7 Budget Line row (Level 3) BudgetLineRow, 314–358 self YES (filtered out via visibleLineIds)
8 Sum row (overall) 1354–1385 Cost uses filteredRawProjected. Payback uses adjustedTotalPayback which is computed from wiTotals + hiTotals payback (project-wide). PARTIAL — cost column OK; payback column and net column wrong
9 Available Funds row 1388–1423 filteredAvailableFunds YES
10 Remaining Budget row 1583–1624 Cost column: filteredAvailableFunds - filteredRawProjected (OK). Net column: same plus adjustedTotalPayback (project-wide). PARTIAL — net column wrong because payback is unfiltered

Out-of-scope rows (per architect decision on #1356):

  • Source detail toggle rows (1444–1505, 1514–1576) — by design show per-source full-project allocated cost and AC-11 full-project pro-rata payback. They are the filter control.
  • Subsidy Adjustments section (1295–1344) — project-wide by architectural decision.

Reproduction

  1. Open /budget/overview on an instance with multiple budget sources where some lines belong to source A and others to source B (and the total spans multiple work items / areas).
  2. Expand the Cost Breakdown table; click the "Available Funds" caret to reveal the per-source toggle rows.
  3. Click any source row to deselect it. The row gets the deselected styling and individual budget lines from that source disappear.
  4. Expand the work items section, then any area, then any work item.
  5. Observe: the Level 2 work-item row Cost/Payback/Net still show the full pre-filter totals for that work item (including the now-hidden lines). The Level 1 area subtotals still show full pre-filter totals. The Level 0 "Work Items" / "Household Items" section headers still show the project-wide totals. The Sum row's Payback and Net columns still include payback from the filtered-out source.
  6. The visible math no longer adds up: child line costs sum to less than the parent area / item subtotal, which sums to less than the section header.

Acceptance criteria

  • AC-1 — Given the filter is active, when any work-item Level 2 row renders, then its Cost column equals the sum of perspective-resolved costs of the budget lines under that work item that are NOT in deselectedSourceIds.
  • AC-2 — Given the filter is active, when any household-item Level 2 row renders, then its Cost column equals the sum of perspective-resolved costs of the budget lines under that household item that are NOT in deselectedSourceIds.
  • AC-3 — Given the filter is active, when any work-item Level 2 row renders, then its Payback column reflects only payback contributed by visible lines on that item (using the same per-source pro-rata weighting as computePerSourcePayback summed across selected sources).
  • AC-4 — Given the filter is active, when any household-item Level 2 row renders, then its Payback column reflects only payback contributed by visible lines on that item.
  • AC-5 — Given the filter is active, when any work-item Level 2 row renders, then its Net column equals (filtered payback) - (filtered cost) per AC-1 and AC-3.
  • AC-6 — Given the filter is active, when any household-item Level 2 row renders, then its Net column equals (filtered payback) - (filtered cost) per AC-2 and AC-4.
  • AC-7 — Given the filter is active, when a Level 1 work-item area subtotal renders (at any depth, including nested children), then its Cost / Payback / Net columns equal the sum of the corresponding filtered values of all visible items and visible descendant areas under it.
  • AC-8 — Given the filter is active, when a Level 1 household-item area subtotal renders (at any depth, including nested children), then its Cost / Payback / Net columns equal the sum of the corresponding filtered values of all visible items and visible descendant areas under it.
  • AC-9 — Given the filter is active, when the Level 0 "Work Items" section header renders, then its Cost / Payback / Net columns equal the sum of the corresponding filtered values of all visible top-level work-item areas.
  • AC-10 — Given the filter is active, when the Level 0 "Household Items" section header renders, then its Cost / Payback / Net columns equal the sum of the corresponding filtered values of all visible top-level household-item areas.
  • AC-11 — Given the filter is active, when the Sum row renders, then both its Payback and Net columns reflect only payback from selected sources (the Cost column already uses filteredRawProjected and remains correct).
  • AC-12 — Given the filter is active, when the Remaining Budget row renders, then its Net column reflects filteredAvailableFunds - filteredRawProjected + filteredAdjustedTotalPayback where filteredAdjustedTotalPayback reflects only payback from selected sources.
  • AC-13 — Given the filter is INACTIVE (no deselected sources), when any of the rows above render, then values are identical to the pre-Available Funds source rows act as per-source filter; Cost/Payback/Net per source #1356 behavior (regression check).
  • AC-14 — Given the filter is active and a partial selection results in numerically consistent rows, when the user expands every level, then for every parent row the sum of its visible immediate children's Cost (and Payback, and Net) equals the parent's displayed value (rounded to whole-currency display precision). This is the canonical "internal consistency" check.

Out of scope

Notes for dev-team-lead

  • A clean implementation would centralize the computation: walk the visible-line set once and produce a tree of {cost, payback, net} aggregates keyed by area / item / section so every row pulls from the same precomputed map. This avoids re-walking the tree per row.
  • computePerSourcePayback (lines 89–142) already does pro-rata per-source attribution; the same pattern can be applied per entity to derive a "filtered payback per item / area / section" via Σ over selected sources of the per-source attribution.
  • For invoiced / quoted entities (item.costDisplay === 'actual' or 'quoted'), per-line filtering still applies because budget lines retain budgetSourceId and hasInvoice / isQuotation flags individually. Decide and document whether item-level cost remains item.actualCost (project-wide) when the item has any invoiced line, or whether it becomes a sum of visible lines' resolved costs. AC-14 (internal consistency) likely forces the latter.

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions