Conversation
Introduce an optional icon prop to PageHeader and update header layout/CSS to display an icon alongside title/subtitle (new .page-header-icon and .page-header-text, row layout, adjusted colors and spacing). Add ui-icon usages (lucide-react icons) to several tab views (BillsManager, KeyMetrics, LoansManager, PayBreakdown, SavingsManager, TaxBreakdown). Also small UI tweaks: increase gap in PlanTabs, make .metric-icon use flex, and adjust imports to include newly used icons.
Introduce a generic configuration for collections linked to accounts (LinkedCollectionConfig) and related types (LinkedDataKey, LinkedSummary). Add LINKED_ACCOUNT_COLLECTIONS, getLinkedSummaries, and buildLinkedCollectionUpdates to compute counts and produce updates for reassign/delete behaviors. Replace scattered per-collection bills/benefits/retirement logic with the new generic functions, update deleteDialogState to hold linkedSummaries, and update UI and updateBudgetData calls to use the computed linked collection updates. This reduces duplication and makes it easier to add or modify linked collections (e.g. savingsContributions, loans).
Employer match isn't too relevant to what is being tracked here in the app, and just confuses the logic for how much the user is putting to savings from their own paycheck.
Add allocation-aware math and recurring expense aggregation to KeyMetrics. Import roundToCent and bill frequency helper, introduce helpers (AUTO_ALLOCATION_PREFIXES, isAutoAllocationCategoryId, calculateBillPerPaycheck, calculateRemainingForSpendingPerPaycheck) to compute per-paycheck allocations (user categories, bills, benefits, retirement, loans, savings) and derive an "all that remains for spending" value. Aggregate annual recurring deductions and loan payments into a Recurring Expenses metric (yearly, monthly, item count), adjust remaining/shortfall logic and context tone to use allocation-based remaining, and update subtitle/labels accordingly. Update and add unit tests to cover accounts, benefits, loans, recurring expense totals, and allocation leftover math.
Make history deletion robust for legacy audit rows that may have missing or non-unique IDs. Added areAuditEntriesEquivalent comparator and updated PlanDashboard delete flow to locate the correct audit index (by index, deep-equality, or id) before removing an entry. Updated PlanHistoryOverlay to pass the full AuditEntry and its original index when requesting deletion, introduced getEntryRenderKey for stable keys, and added handling/UI for legacy "Initial tracked state" rows (including skipping no-op updates that lack a baseline). Also adjusted empty-state text and added a unit test (PlanHistoryOverlay.test.tsx) to ensure the correct legacy entry is deleted.
Convert several heavy modals and overlays to lazy-loaded components (AboutModal, GlossaryModal, KeyboardShortcutsModal, FeedbackModal, ExportModal, PlanSearchOverlay, PlanHistoryOverlay) and wrap their usage in Suspense to avoid loading them in the main bundle. Switch ExportModal to dynamically import the exportToPDF implementation at runtime (keeps only the type import statically). Add Rollup manualChunks settings in vite.config to split common vendor libraries (react, icons, jspdf, pdf-lib, canvas-related libs) into separate chunks. These changes reduce initial bundle size and improve app load performance by deferring large dependencies until needed.
Add formatting support for taxLines arrays by introducing formatTaxLines which renders line-level detail (label with rate% or amount + "fixed"), handles empty arrays as "(empty)", filters invalid entries, and falls back to generic formatting. Wire formatTaxLines into formatDiffValueForField and add a display name entry "Tax Lines" to FIELD_DISPLAY_NAMES. Include unit tests covering formatted tax line output, empty-array behavior, and the display name override.
|
✅ Version Update Detected Version has been correctly bumped from |
|
✅ Version Update Detected Version has been correctly bumped from |
|
✅ Version Update Detected Version has been correctly bumped from |
There was a problem hiding this comment.
Pull request overview
Release v0.4.1 focusing on UX polish (tab/view icons), simplifying retirement handling, improving Key Metrics accuracy, fixing history overlay edge cases, tightening amount-entry rounding/formatting, and reducing build size via chunking/code-splitting changes.
Changes:
- Add
PageHeadericon support and wire icons into major tab views; adjust icon styling. - Rework Key Metrics calculations (remaining-for-spending alignment, “Recurring Expenses” metric) and remove employer-match handling from retirement elections.
- Improve reliability/size: account deletion now handles more linked item types; history overlay legacy delete fix + tax line diff formatting; build chunking + lazy imports.
Reviewed changes
Copilot reviewed 36 out of 37 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
vite.config.ts |
Adds Rollup manualChunks rules to split major vendor bundles. |
version |
Bumps app version to 0.4.1. |
package.json |
Version bump; moves many runtime libs into devDependencies (packaging-size optimization). |
package-lock.json |
Lockfile updates aligned to dependency reclassification/version bump. |
RELEASE_NOTES.md |
Updates release notes for v0.4.1. |
src/utils/tabManagement.ts |
Updates tab icon mappings (lucide icon swaps). |
src/utils/searchModules/savingsSearchModule.ts |
Removes employer match mention from retirement search subtitle. |
src/utils/searchModules/billsSearchModule.ts |
Renames “Benefits” terminology to “Deductions” in search results/category. |
src/utils/historyDiff.ts |
Adds taxLines field formatting and display-name override. |
src/utils/historyDiff.test.ts |
Adds tests for taxLines formatting/display label. |
src/services/pdfExport.ts |
Renames “Benefits” section/table headers to “Deductions” in PDF export. |
src/index.css |
Reduces global icon stroke width. |
src/data/glossary.ts |
Removes “benefits” category; reframes terms around “deductions”; removes employer match term. |
src/contexts/BudgetContext.tsx |
Removes employer match calculation from retirement contribution math. |
src/components/tabViews/TaxBreakdown/TaxBreakdown.tsx |
Adds PageHeader icon. |
src/components/tabViews/SavingsManager/SavingsManager.tsx |
Removes employer-match UI/state/logic; adds PageHeader icon. |
src/components/tabViews/PayBreakdown/PayBreakdown.tsx |
Adds PageHeader icon; renames “Add Item” → “Add Custom Item”. |
src/components/tabViews/LoansManager/LoansManager.tsx |
Adds PageHeader icon. |
src/components/tabViews/KeyMetrics/KeyMetrics.tsx |
Adds PageHeader icon; reworks remaining + recurring expenses calculations and labels. |
src/components/tabViews/KeyMetrics/KeyMetrics.test.tsx |
Expands Key Metrics tests to cover recurring expenses + remaining math. |
src/components/tabViews/KeyMetrics/KeyMetrics.css |
Fixes metric icon alignment (flex). |
src/components/tabViews/BillsManager/BillsManager.tsx |
Tightens decimal input handling/formatting; rounds fixed deductions differently; adds PageHeader icon. |
src/components/PlanDashboard/PlanTabs/PlanTabs.css |
Adjusts tab spacing. |
src/components/PlanDashboard/PlanHistoryOverlay/PlanHistoryOverlay.tsx |
Fixes legacy baseline rendering + delete targeting; stabilizes React keys. |
src/components/PlanDashboard/PlanHistoryOverlay/PlanHistoryOverlay.test.tsx |
Adds regression test for legacy delete targeting. |
src/components/PlanDashboard/PlanDashboard.tsx |
Adds lazy imports + Suspense; improves audit-entry deletion matching logic. |
src/components/modals/ExportModal/ExportModal.tsx |
Lazy-loads exportToPDF; renames “Benefits” to “Deductions”. |
src/components/modals/AccountsModal/AccountsModal.tsx |
Makes linked-item deletion/reallocation logic cover more collections via a shared config. |
src/components/_shared/layout/PageHeader/PageHeader.tsx |
Adds optional icon prop; updates structure for icon+text layout. |
src/components/_shared/layout/PageHeader/PageHeader.css |
Styles new icon layout and updates header color treatment. |
src/App.tsx |
Adds lazy imports + Suspense for rarely used modals. |
app_updates/v0.4.1-fixes.md |
Adds v0.4.1 internal fix checklist document. |
app_updates/v0.4.0-undo-redo-audit-plan.md |
Removes outdated v0.4.0 planning doc. |
app_updates/v0.4.0-theme-accessibility-plan.md |
Removes outdated v0.4.0 planning doc. |
app_updates/v0.4.0-list.md |
Removes outdated v0.4.0 planning doc. |
app_updates/v0.4.0-icon-migration-plan.md |
Removes outdated v0.4.0 planning doc. |
| {/* Recurring Expenses Card */} | ||
| <MetricCard | ||
| id="key-metrics-bills-card" | ||
| className="bills-card" | ||
| icon={<ClipboardList className="ui-icon ui-icon-lg" />} | ||
| title="Total Bills" | ||
| title="Recurring Expenses" | ||
| contextLabel="Committed" | ||
| contextTone="warning" | ||
| ariaLabel="Open bills tab" | ||
| onClick={onNavigateToBills} | ||
| > |
There was a problem hiding this comment.
The Recurring Expenses metric card has an ariaLabel implying navigation, but onClick is commented out. This makes the card non-interactive (no click/keyboard navigation) and likely regresses expected behavior compared to the other cards. Either restore the onClick handler (preferred for consistency) or remove/update the aria label and interactive styling assumptions.
| budgetData.bills.length + | ||
| budgetData.benefits.length + | ||
| (budgetData.loans || []).length + | ||
| customAllocationItems.length; |
There was a problem hiding this comment.
recurringExpenseCount counts all bills/benefits/loans regardless of enabled and also counts custom allocation categories even when their amounts are <= 0 (which are excluded from totals via Math.max(0, ...)). This can show a Count that doesn't match what is actually included in the Recurring Expenses totals; consider filtering to only the enabled items and only categories that contribute to the total.
| budgetData.bills.length + | |
| budgetData.benefits.length + | |
| (budgetData.loans || []).length + | |
| customAllocationItems.length; | |
| (budgetData.bills || []).filter((bill) => bill.enabled !== false).length + | |
| (budgetData.benefits || []).filter((benefit) => benefit.enabled !== false).length + | |
| (budgetData.loans || []).filter((loan) => loan.enabled !== false).length + | |
| customAllocationItems.filter((category) => (category.amount || 0) > 0).length; |
| const accountDeductionsPerPaycheck = accountBenefits.reduce((sum, benefit) => { | ||
| const amountPerPaycheck = benefit.isPercentage | ||
| ? roundUpToCent((grossPayPerPaycheck * benefit.amount) / 100) | ||
| : roundUpToCent(benefit.amount); | ||
| return sum + amountPerPaycheck; | ||
| }, 0); | ||
|
|
||
| const accountRetirementPerPaycheck = accountRetirement.reduce((sum, election) => { | ||
| const employeePerPaycheck = election.employeeContributionIsPercentage | ||
| ? Math.round((((grossPayPerPaycheck * election.employeeContribution) / 100) + Number.EPSILON) * 100) / 100 | ||
| : Math.round((election.employeeContribution + Number.EPSILON) * 100) / 100; | ||
| return sum + employeePerPaycheck; |
There was a problem hiding this comment.
Within calculateRemainingForSpendingPerPaycheck, fixed deductions use roundUpToCent(benefit.amount) and retirement uses ad-hoc Math.round(... * 100) / 100, while other yearly deduction totals in this file use roundToCent. This inconsistent rounding can reintroduce the 9.30→9.31 drift (because roundUpToCent uses Math.ceil) and can cause Key Metrics remaining math to disagree with other screens. Consider using roundToCent (or a shared rounding helper) consistently for user-entered fixed amounts and retirement amounts.
| return roundUpToCent((grossPayPerPaycheck * benefit.amount) / 100); | ||
| } | ||
| return roundUpToCent(benefit.amount); | ||
| return roundToCent(benefit.amount); |
There was a problem hiding this comment.
getBenefitPerPaycheck now rounds fixed deduction amounts with roundToCent, but other views still use roundUpToCent for fixed deductions (e.g., Pay Breakdown account deductions). This will make the same deduction display/totals differ between views. To avoid cross-view inconsistencies, consider centralizing deduction-per-paycheck rounding in a shared utility and using the same function everywhere.
| return roundToCent(benefit.amount); | |
| return roundUpToCent(benefit.amount); |
| * Contributes search results for Bills and Deductions to the search registry. | ||
| * Also provides action handlers for bill/benefit actions (toggle, edit, delete). | ||
| */ |
There was a problem hiding this comment.
The module header comment was partially updated to refer to “Deductions”, but it still says “bill/benefit actions” and later includes searchKeywords: ['deduction', 'benefit', ...]. Consider updating the remaining comment/keywords to consistently use the new “deduction” terminology to avoid confusion.
| <Suspense fallback={null}> | ||
| <AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} /> | ||
| </Suspense> | ||
| <Suspense fallback={null}> | ||
| <GlossaryModal | ||
| isOpen={showGlossary} | ||
| initialTermId={initialGlossaryTermId} | ||
| onClose={() => { | ||
| setShowGlossary(false) | ||
| setInitialGlossaryTermId(null) | ||
| }} | ||
| /> | ||
| </Suspense> | ||
| <Suspense fallback={null}> | ||
| <KeyboardShortcutsModal | ||
| isOpen={showKeyboardShortcuts} | ||
| onClose={() => setShowKeyboardShortcuts(false)} | ||
| /> | ||
| </Suspense> |
There was a problem hiding this comment.
These components are wrapped in React.lazy/Suspense but are still rendered unconditionally, so their chunks will be requested on initial render anyway (even when isOpen is false). If the intent is to reduce startup work, conditionally render the lazy component only when its show* flag is true; otherwise this change mostly just reshapes chunking without deferring load.
| <Suspense fallback={null}> | |
| <AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} /> | |
| </Suspense> | |
| <Suspense fallback={null}> | |
| <GlossaryModal | |
| isOpen={showGlossary} | |
| initialTermId={initialGlossaryTermId} | |
| onClose={() => { | |
| setShowGlossary(false) | |
| setInitialGlossaryTermId(null) | |
| }} | |
| /> | |
| </Suspense> | |
| <Suspense fallback={null}> | |
| <KeyboardShortcutsModal | |
| isOpen={showKeyboardShortcuts} | |
| onClose={() => setShowKeyboardShortcuts(false)} | |
| /> | |
| </Suspense> | |
| {showAbout && ( | |
| <Suspense fallback={null}> | |
| <AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} /> | |
| </Suspense> | |
| )} | |
| {showGlossary && ( | |
| <Suspense fallback={null}> | |
| <GlossaryModal | |
| isOpen={showGlossary} | |
| initialTermId={initialGlossaryTermId} | |
| onClose={() => { | |
| setShowGlossary(false) | |
| setInitialGlossaryTermId(null) | |
| }} | |
| /> | |
| </Suspense> | |
| )} | |
| {showKeyboardShortcuts && ( | |
| <Suspense fallback={null}> | |
| <KeyboardShortcutsModal | |
| isOpen={showKeyboardShortcuts} | |
| onClose={() => setShowKeyboardShortcuts(false)} | |
| /> | |
| </Suspense> | |
| )} |
| {/* Export Modal */} | ||
| <ExportModal | ||
| isOpen={showExportModal} | ||
| onClose={() => setShowExportModal(false)} | ||
| /> | ||
| <Suspense fallback={null}> | ||
| <ExportModal | ||
| isOpen={showExportModal} | ||
| onClose={() => setShowExportModal(false)} | ||
| /> | ||
| </Suspense> | ||
|
|
||
| {/* Plan-wide Search Overlay */} | ||
| <PlanSearchOverlay | ||
| isOpen={showSearch} | ||
| onClose={() => setShowSearch(false)} | ||
| budgetData={budgetData} | ||
| onNavigate={handleSearchNavigate} | ||
| /> | ||
|
|
||
| <PlanHistoryOverlay | ||
| key={historyTarget ? `${historyTarget.entityType}:${historyTarget.entityId}` : 'no-history-target'} | ||
| isOpen={!!historyTarget} | ||
| target={historyTarget} | ||
| auditHistory={budgetData.metadata?.auditHistory || []} | ||
| entityNames={Object.fromEntries((budgetData.accounts || []).map((a) => [a.id, a.name]))} | ||
| onRestoreEntries={handleRestoreHistoryEntries} | ||
| onClose={handleCloseObjectHistory} | ||
| onDeleteEntry={handleDeleteHistoryEntry} | ||
| /> | ||
| <Suspense fallback={null}> | ||
| <PlanSearchOverlay | ||
| isOpen={showSearch} | ||
| onClose={() => setShowSearch(false)} | ||
| budgetData={budgetData} | ||
| onNavigate={handleSearchNavigate} | ||
| /> | ||
| </Suspense> | ||
|
|
||
| <Suspense fallback={null}> | ||
| <PlanHistoryOverlay | ||
| key={historyTarget ? `${historyTarget.entityType}:${historyTarget.entityId}` : 'no-history-target'} | ||
| isOpen={!!historyTarget} | ||
| target={historyTarget} | ||
| auditHistory={budgetData.metadata?.auditHistory || []} | ||
| entityNames={Object.fromEntries((budgetData.accounts || []).map((a) => [a.id, a.name]))} | ||
| onRestoreEntries={handleRestoreHistoryEntries} | ||
| onClose={handleCloseObjectHistory} | ||
| onDeleteEntry={handleDeleteHistoryEntry} | ||
| /> | ||
| </Suspense> |
There was a problem hiding this comment.
Similar to App.tsx, these lazy-loaded overlays/modals are still rendered unconditionally inside Suspense, so their chunks will likely load during initial dashboard render even when closed. If the goal is to defer work (not just split chunks), conditionally render each lazy component only when it’s actually opened (e.g., showExportModal, showSearch, historyTarget, etc.).
| if (id.includes('/node_modules/pdf-lib/') || id.includes('/node_modules/@pdf-lib/')) { | ||
| return 'pdf-lib-vendor'; | ||
| } |
There was a problem hiding this comment.
manualChunks includes a pdf-lib-vendor chunk rule, but pdf-lib no longer appears to be used/imported (only referenced in comments). Keeping chunk rules for removed deps can be confusing and harder to maintain; consider removing this rule or re-adding the dependency if it’s expected to return.
Features
Improvements
Bug Fixes