feat: Improve Balance View category breakdown with paid/expected distinction #241

Closed
Copilot wants to merge 5 commits from copilot/improve-balance-view-presentation into main
Copilot commented 2026-05-02 08:45:03 +00:00 (Migrated from github.com)

The Balance View category totals showed only the actualized/paid sum when collapsed, making it impossible to see the true projected spend for a category at a glance. When expanded, paid and unpaid transactions were visually identical.

Description

Changes are isolated to CategorySummaryList.vue — no backend or data-shape changes required. The existing transaction DTO fields (actual_amount, expected_amount, is_virtual, is_deferred) already carry everything needed.

New totals model per category

Replaced the single actualTotal/expectedTotal pair with three purpose-built values:

  • paidTotal — sum of actual_amount for settled transactions
  • expectedRemainingTotal — sum of expected_amount for unpaid/virtual transactions
  • budgetTotal = paidTotal + expectedRemainingTotal — effective projected spend

Collapsed category header

Now leads with budgetTotal and surfaces contextual chips:

  • Paid: $X (green) when any items are settled
  • Expected: $Y (blue) when any items are outstanding

Individual transaction rows

State is now visually unambiguous:

  • Paid: amount in green + "Paid ✓" chip
  • Unpaid/virtual: amount in muted grey + "Expected" chip (blue)
  • Deferred: "Deferred" chip (amber)

Category total & Grand total

Both show the three-part breakdown: budgetTotal headline + "Paid: X" + "Expected: Y" sub-lines.

Sorting

Changed from actualTotal to budgetTotal so categories with large unpaid balances rank correctly.

TypeScript & code quality improvements (based on review feedback)

  • Added BalanceCategoryTransaction, EnrichedTransaction, and CategorySummary interfaces — all any types removed; the grouping Map is fully typed as Map<string, CategorySummary>
  • isTransactionPaid() is now evaluated once per transaction during categorySummaries computation and stored as paid: boolean on each EnrichedTransaction; the template references transaction.paid throughout with no repeated calls
  • Replaced || fallbacks with ?? (nullish coalescing) so expected_amount = 0 is treated correctly instead of falling back to amount
  • Replaced parseFloat(String(x)) patterns with the cleaner Number(x)
  • Collapsed header chips rewritten to use standard closing tags, removing potential stray > text node risk

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (code change that neither fixes a bug nor adds a feature)
  • Performance improvement
  • Test coverage improvement

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this manually (if applicable)

Code Quality

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings or errors

CI/CD Checks

  • All automated tests pass
  • Code formatting (Pint) passes
  • JavaScript/TypeScript linting passes
  • Build process completes successfully

Additional Notes

All changes are confined to CategorySummaryList.vue. The component now enforces TypeScript contracts on the transaction and category summary shapes, making future modifications to the paid/expected logic less error-prone.

The Balance View category totals showed only the actualized/paid sum when collapsed, making it impossible to see the true projected spend for a category at a glance. When expanded, paid and unpaid transactions were visually identical. ## Description Changes are isolated to `CategorySummaryList.vue` — no backend or data-shape changes required. The existing transaction DTO fields (`actual_amount`, `expected_amount`, `is_virtual`, `is_deferred`) already carry everything needed. ### New totals model per category Replaced the single `actualTotal`/`expectedTotal` pair with three purpose-built values: - `paidTotal` — sum of `actual_amount` for settled transactions - `expectedRemainingTotal` — sum of `expected_amount` for unpaid/virtual transactions - `budgetTotal` = `paidTotal + expectedRemainingTotal` — effective projected spend ### Collapsed category header Now leads with `budgetTotal` and surfaces contextual chips: - `Paid: $X` (green) when any items are settled - `Expected: $Y` (blue) when any items are outstanding ### Individual transaction rows State is now visually unambiguous: - Paid: amount in green + "Paid ✓" chip - Unpaid/virtual: amount in muted grey + "Expected" chip (blue) - Deferred: "Deferred" chip (amber) ### Category total & Grand total Both show the three-part breakdown: `budgetTotal` headline + "Paid: X" + "Expected: Y" sub-lines. ### Sorting Changed from `actualTotal` to `budgetTotal` so categories with large unpaid balances rank correctly. ### TypeScript & code quality improvements (based on review feedback) - Added `BalanceCategoryTransaction`, `EnrichedTransaction`, and `CategorySummary` interfaces — all `any` types removed; the grouping `Map` is fully typed as `Map<string, CategorySummary>` - `isTransactionPaid()` is now evaluated once per transaction during `categorySummaries` computation and stored as `paid: boolean` on each `EnrichedTransaction`; the template references `transaction.paid` throughout with no repeated calls - Replaced `||` fallbacks with `??` (nullish coalescing) so `expected_amount = 0` is treated correctly instead of falling back to `amount` - Replaced `parseFloat(String(x))` patterns with the cleaner `Number(x)` - Collapsed header chips rewritten to use standard closing tags, removing potential stray `>` text node risk ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [x] Refactoring (code change that neither fixes a bug nor adds a feature) - [ ] Performance improvement - [ ] Test coverage improvement ## Testing - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have tested this manually (if applicable) ## Code Quality - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] My changes generate no new warnings or errors ## CI/CD Checks - [ ] All automated tests pass - [ ] Code formatting (Pint) passes - [x] JavaScript/TypeScript linting passes - [ ] Build process completes successfully ## Additional Notes All changes are confined to `CategorySummaryList.vue`. The component now enforces TypeScript contracts on the transaction and category summary shapes, making future modifications to the paid/expected logic less error-prone.
niddelicious (Migrated from github.com) reviewed 2026-05-02 08:45:03 +00:00
coderabbitai[bot] commented 2026-05-02 08:45:09 +00:00 (Migrated from github.com)

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: ee17d4d0-ad3a-438d-b909-d17fa66974b7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands and usage tips.

<!-- This is an auto-generated comment: summarize by coderabbit.ai --> <!-- This is an auto-generated comment: skip review by coderabbit.ai --> > [!IMPORTANT] > ## Review skipped > > Bot user detected. > > To trigger a single review, invoke the `@coderabbitai review` command. > > <details> > <summary>⚙️ Run configuration</summary> > > **Configuration used**: defaults > > **Review profile**: CHILL > > **Plan**: Free > > **Run ID**: `ee17d4d0-ad3a-438d-b909-d17fa66974b7` > > </details> > > You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file. > > Use the checkbox below for a quick retry: > - [ ] <!-- {"checkboxId": "e9bb8d72-00e8-4f67-9cb2-caf3b22574fe"} --> 🔍 Trigger review <!-- end of auto-generated comment: skip review by coderabbit.ai --> <!-- tips_start --> --- <sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub> <!-- tips_end -->
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2026-05-02 08:52:43 +00:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR improves the Balance View’s category and transaction presentation by distinguishing paid vs expected amounts, and by basing collapsed totals/sorting on an effective “budget” total rather than only actualized spend.

Changes:

  • Introduces per-category paidTotal, expectedRemainingTotal, and budgetTotal, with new grand totals derived from them.
  • Updates UI to visually differentiate paid vs expected transactions and show contextual “Paid/Expected” chips in collapsed and total rows.
  • Updates sorting and percentage calculations to use budgetTotal.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
resources/js/Composables/useDate.ts Formatting-only change to totalDays assignment.
resources/js/Components/CategorySummaryList.vue Implements paid/expected/budget totals and updates category + transaction UI to reflect settlement state.
resources/js/Components/Calendar/TaskChip.vue Formatting-only change to template interpolation line breaks.
DEVLOG.md Documents the Balance View presentation improvements.
CHANGELOG.md Adds user-facing release notes for the Balance View breakdown clarity.
## Pull request overview > [!NOTE] > Copilot was unable to run its full agentic suite in this review. This PR improves the Balance View’s category and transaction presentation by distinguishing **paid** vs **expected** amounts, and by basing collapsed totals/sorting on an effective “budget” total rather than only actualized spend. **Changes:** - Introduces per-category `paidTotal`, `expectedRemainingTotal`, and `budgetTotal`, with new grand totals derived from them. - Updates UI to visually differentiate paid vs expected transactions and show contextual “Paid/Expected” chips in collapsed and total rows. - Updates sorting and percentage calculations to use `budgetTotal`. ### Reviewed changes Copilot reviewed 5 out of 5 changed files in this pull request and generated 14 comments. <details> <summary>Show a summary per file</summary> | File | Description | | ---- | ----------- | | resources/js/Composables/useDate.ts | Formatting-only change to `totalDays` assignment. | | resources/js/Components/CategorySummaryList.vue | Implements paid/expected/budget totals and updates category + transaction UI to reflect settlement state. | | resources/js/Components/Calendar/TaskChip.vue | Formatting-only change to template interpolation line breaks. | | DEVLOG.md | Documents the Balance View presentation improvements. | | CHANGELOG.md | Adds user-facing release notes for the Balance View breakdown clarity. | </details>
@ -8,0 +17,4 @@
}
interface BalanceCategoryTransaction {
id?: string | number;
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:42 +00:00

New logic adds more fields and rules but relies on any for both transaction and the grouped summary model, which makes it easier to introduce regressions (e.g., mistyped field names like actual_amount/expected_amount, or missing booleans). Define (or reuse) a Transaction type and a CategorySummary type (and type the map as Map<string, CategorySummary>) so TypeScript can enforce the new totals contract.

New logic adds more fields and rules but relies on `any` for both `transaction` and the grouped summary model, which makes it easier to introduce regressions (e.g., mistyped field names like `actual_amount`/`expected_amount`, or missing booleans). Define (or reuse) a `Transaction` type and a `CategorySummary` type (and type the map as `Map<string, CategorySummary>`) so TypeScript can enforce the new totals contract.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:43 +00:00

New logic adds more fields and rules but relies on any for both transaction and the grouped summary model, which makes it easier to introduce regressions (e.g., mistyped field names like actual_amount/expected_amount, or missing booleans). Define (or reuse) a Transaction type and a CategorySummary type (and type the map as Map<string, CategorySummary>) so TypeScript can enforce the new totals contract.

New logic adds more fields and rules but relies on `any` for both `transaction` and the grouped summary model, which makes it easier to introduce regressions (e.g., mistyped field names like `actual_amount`/`expected_amount`, or missing booleans). Define (or reuse) a `Transaction` type and a `CategorySummary` type (and type the map as `Map<string, CategorySummary>`) so TypeScript can enforce the new totals contract.
@ -8,0 +41,4 @@
/** Sum of actual_amount for paid/actualized items */
paidTotal: number;
/** Sum of expected_amount for unpaid/virtual items */
expectedRemainingTotal: number;
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:41 +00:00

transaction.expected_amount || transaction.amount incorrectly falls back when expected_amount is 0 (because 0 is falsy), which can inflate totals and display the wrong value. Prefer nullish coalescing (or an explicit null/undefined check) so that 0 is treated as a valid expected amount in both the totals calculation and the transaction-row amount display.

`transaction.expected_amount || transaction.amount` incorrectly falls back when `expected_amount` is `0` (because `0` is falsy), which can inflate totals and display the wrong value. Prefer nullish coalescing (or an explicit null/undefined check) so that `0` is treated as a valid expected amount in both the totals calculation and the transaction-row amount display.
@ -83,0 +139,4 @@
const grandTotalExpectedRemaining = computed(() =>
categorySummaries.value.reduce(
(sum, c) => sum + c.expectedRemainingTotal,
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:41 +00:00

There are stray > characters between sibling <v-chip> elements (e.g., </v-chip then > <v-chip). In Vue templates this can render a literal > text node in the UI. Remove those extra > separators and place sibling components adjacently as proper elements (newline/whitespace only) to avoid unwanted characters in the rendered output.

There are stray `>` characters between sibling `<v-chip>` elements (e.g., `</v-chip` then `> <v-chip`). In Vue templates this can render a literal `>` text node in the UI. Remove those extra `>` separators and place sibling components adjacently as proper elements (newline/whitespace only) to avoid unwanted characters in the rendered output.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:42 +00:00

There are stray > characters between sibling <v-chip> elements (e.g., </v-chip then > <v-chip). In Vue templates this can render a literal > text node in the UI. Remove those extra > separators and place sibling components adjacently as proper elements (newline/whitespace only) to avoid unwanted characters in the rendered output.

There are stray `>` characters between sibling `<v-chip>` elements (e.g., `</v-chip` then `> <v-chip`). In Vue templates this can render a literal `>` text node in the UI. Remove those extra `>` separators and place sibling components adjacently as proper elements (newline/whitespace only) to avoid unwanted characters in the rendered output.
@ -133,3 +189,1 @@
> {{ getPercentage(category.actualTotal) }}%
</v-chip
>
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:41 +00:00

transaction.expected_amount || transaction.amount incorrectly falls back when expected_amount is 0 (because 0 is falsy), which can inflate totals and display the wrong value. Prefer nullish coalescing (or an explicit null/undefined check) so that 0 is treated as a valid expected amount in both the totals calculation and the transaction-row amount display.

`transaction.expected_amount || transaction.amount` incorrectly falls back when `expected_amount` is `0` (because `0` is falsy), which can inflate totals and display the wrong value. Prefer nullish coalescing (or an explicit null/undefined check) so that `0` is treated as a valid expected amount in both the totals calculation and the transaction-row amount display.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:42 +00:00

There are stray > characters between sibling <v-chip> elements (e.g., </v-chip then > <v-chip). In Vue templates this can render a literal > text node in the UI. Remove those extra > separators and place sibling components adjacently as proper elements (newline/whitespace only) to avoid unwanted characters in the rendered output.

There are stray `>` characters between sibling `<v-chip>` elements (e.g., `</v-chip` then `> <v-chip`). In Vue templates this can render a literal `>` text node in the UI. Remove those extra `>` separators and place sibling components adjacently as proper elements (newline/whitespace only) to avoid unwanted characters in the rendered output.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:42 +00:00

There are stray > characters between sibling <v-chip> elements (e.g., </v-chip then > <v-chip). In Vue templates this can render a literal > text node in the UI. Remove those extra > separators and place sibling components adjacently as proper elements (newline/whitespace only) to avoid unwanted characters in the rendered output.

There are stray `>` characters between sibling `<v-chip>` elements (e.g., `</v-chip` then `> <v-chip`). In Vue templates this can render a literal `>` text node in the UI. Remove those extra `>` separators and place sibling components adjacently as proper elements (newline/whitespace only) to avoid unwanted characters in the rendered output.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:42 +00:00

There are stray > characters between sibling <v-chip> elements (e.g., </v-chip then > <v-chip). In Vue templates this can render a literal > text node in the UI. Remove those extra > separators and place sibling components adjacently as proper elements (newline/whitespace only) to avoid unwanted characters in the rendered output.

There are stray `>` characters between sibling `<v-chip>` elements (e.g., `</v-chip` then `> <v-chip`). In Vue templates this can render a literal `>` text node in the UI. Remove those extra `>` separators and place sibling components adjacently as proper elements (newline/whitespace only) to avoid unwanted characters in the rendered output.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:42 +00:00

There are stray > characters between sibling <v-chip> elements (e.g., </v-chip then > <v-chip). In Vue templates this can render a literal > text node in the UI. Remove those extra > separators and place sibling components adjacently as proper elements (newline/whitespace only) to avoid unwanted characters in the rendered output.

There are stray `>` characters between sibling `<v-chip>` elements (e.g., `</v-chip` then `> <v-chip`). In Vue templates this can render a literal `>` text node in the UI. Remove those extra `>` separators and place sibling components adjacently as proper elements (newline/whitespace only) to avoid unwanted characters in the rendered output.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:43 +00:00

isPaid(transaction) is called multiple times per row in the template, which duplicates logic and makes future changes more error-prone (e.g., if the paid criteria changes). Consider computing it once per transaction row (e.g., derive a paid boolean when building categorySummaries, or bind via a small wrapper/helper for the template) and reference that single value throughout.

`isPaid(transaction)` is called multiple times per row in the template, which duplicates logic and makes future changes more error-prone (e.g., if the paid criteria changes). Consider computing it once per transaction row (e.g., derive a `paid` boolean when building `categorySummaries`, or bind via a small wrapper/helper for the template) and reference that single value throughout.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:43 +00:00

isPaid(transaction) is called multiple times per row in the template, which duplicates logic and makes future changes more error-prone (e.g., if the paid criteria changes). Consider computing it once per transaction row (e.g., derive a paid boolean when building categorySummaries, or bind via a small wrapper/helper for the template) and reference that single value throughout.

`isPaid(transaction)` is called multiple times per row in the template, which duplicates logic and makes future changes more error-prone (e.g., if the paid criteria changes). Consider computing it once per transaction row (e.g., derive a `paid` boolean when building `categorySummaries`, or bind via a small wrapper/helper for the template) and reference that single value throughout.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:43 +00:00

isPaid(transaction) is called multiple times per row in the template, which duplicates logic and makes future changes more error-prone (e.g., if the paid criteria changes). Consider computing it once per transaction row (e.g., derive a paid boolean when building categorySummaries, or bind via a small wrapper/helper for the template) and reference that single value throughout.

`isPaid(transaction)` is called multiple times per row in the template, which duplicates logic and makes future changes more error-prone (e.g., if the paid criteria changes). Consider computing it once per transaction row (e.g., derive a `paid` boolean when building `categorySummaries`, or bind via a small wrapper/helper for the template) and reference that single value throughout.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-02 08:52:43 +00:00

isPaid(transaction) is called multiple times per row in the template, which duplicates logic and makes future changes more error-prone (e.g., if the paid criteria changes). Consider computing it once per transaction row (e.g., derive a paid boolean when building categorySummaries, or bind via a small wrapper/helper for the template) and reference that single value throughout.

`isPaid(transaction)` is called multiple times per row in the template, which duplicates logic and makes future changes more error-prone (e.g., if the paid criteria changes). Consider computing it once per transaction row (e.g., derive a `paid` boolean when building `categorySummaries`, or bind via a small wrapper/helper for the template) and reference that single value throughout.
niddelicious commented 2026-05-02 15:24:08 +00:00 (Migrated from github.com)

@copilot apply changes based on the comments in this thread

@copilot apply changes based on the comments in [this thread](https://github.com/niddelicious/parenting-tool/pull/241#pullrequestreview-4214765977)
Copilot commented 2026-05-02 15:33:13 +00:00 (Migrated from github.com)

@copilot apply changes based on the comments in this thread

Applied all four reviewer feedback items in commit ebd2a6a:

  1. TypeScript types — Added BalanceCategoryTransaction, EnrichedTransaction, and CategorySummary interfaces; the Map is now typed as Map<string, CategorySummary> and the prop is BalanceCategoryTransaction[] instead of any[]
  2. Pre-computed paid per transactionisTransactionPaid() is now called once per transaction in the script (const paid = isTransactionPaid(transaction)), stored on each EnrichedTransaction, and the template references transaction.paid throughout — zero isPaid() calls remain in the template
  3. ||?? — All expected_amount || amount fallbacks are now expected_amount ?? amount so a zero expected amount is handled correctly; also cleaned up parseFloat(String(x)) to the simpler Number(x) per follow-up review feedback
  4. Stray > chips — Collapsed header chips rewritten to use standard </v-chip> closing tags; linter (Prettier + prettier-plugin-vue) confirmed clean
> @copilot apply changes based on the comments in [this thread](https://github.com/niddelicious/parenting-tool/pull/241#pullrequestreview-4214765977) Applied all four reviewer feedback items in commit `ebd2a6a`: 1. **TypeScript types** — Added `BalanceCategoryTransaction`, `EnrichedTransaction`, and `CategorySummary` interfaces; the `Map` is now typed as `Map<string, CategorySummary>` and the prop is `BalanceCategoryTransaction[]` instead of `any[]` 2. **Pre-computed `paid` per transaction** — `isTransactionPaid()` is now called once per transaction in the script (`const paid = isTransactionPaid(transaction)`), stored on each `EnrichedTransaction`, and the template references `transaction.paid` throughout — zero `isPaid()` calls remain in the template 3. **`||` → `??`** — All `expected_amount || amount` fallbacks are now `expected_amount ?? amount` so a zero expected amount is handled correctly; also cleaned up `parseFloat(String(x))` to the simpler `Number(x)` per follow-up review feedback 4. **Stray `>` chips** — Collapsed header chips rewritten to use standard `</v-chip>` closing tags; linter (Prettier + prettier-plugin-vue) confirmed clean

Pull request closed

Sign in to join this conversation.
No description provided.