Skip to content

New Calendar Tab #95

@kryptodrex

Description

@kryptodrex

Due Dates & Cash Flow Calendar

Add due-date awareness to bills and loans so the app can show when money leaves each account — not just how much. Culminates in a calendar view that plots paycheck deposits alongside obligation withdrawals, giving users a time-aware picture of their cash flow.

Problem Statement

1. Bills and loans have no "when"

Bills and loans track how much and how often, but not which day of the cycle the payment is due. A user with $1,200/month in bills from a checking account knows they need ~$553.85 per paycheck to cover it, but they don't know whether all $1,200 is due on the 1st, spread across the month, or back-loaded on the 28th. This matters: if most bills hit the account before the second paycheck of the month arrives, the account can go negative even though the monthly math is fine.

2. No temporal view of cash flow

The app's existing views are all amount-centric (per-paycheck, monthly average, annual). There is no view that plots events on a timeline. Users who want to answer "will my checking account be positive on the 15th?" have to do that math outside the app.

3. The data model is almost ready

Bill already has an orphaned dueDay?: number field (defined in src/types/obligations.ts but never exposed in the UI). Loan has startDate and paymentFrequency but no payment day. The plumbing is partially there — it just needs to be surfaced and extended.

Solution Overview

Three phases, each independently useful:

  1. Due-date fields on bills and loans — surface dueDay in the bill form and add paymentDueDay to the loan form. Optional field; everything continues to work without it. No file format migration needed for bills (field already exists). Loans need one new optional field.
  2. Cash flow projection engine — a pure utility that takes bill/loan due dates, paycheck dates, and account assignments, then produces a chronological ledger of deposits (paychecks) and withdrawals (bills/loans) for any date range. This is the analytical core.
  3. Calendar tab view — a new calendar tab in the dashboard that renders the cash flow projection as an interactive monthly calendar. Shows paycheck deposits in green, bill/loan withdrawals in red, running account balances, and highlights days where an account would go negative.

What this plan does NOT do

  • No automatic bill-pay scheduling or reminders/notifications. This is a planning tool, not a payment tool.
  • No recurring-event engine beyond what payCalendar.ts already provides. Bill due dates are a simple day-of-month (or day-of-cycle for non-monthly frequencies); we don't need a full calendar recurrence engine.
  • No changes to the stable allocation math from Phase 4 of v0.5.0. The per-paycheck allocation is still the recommended approach; the calendar view just shows the user whether that approach holds up temporally.
  • No multi-month or yearly calendar panorama in Phase 3. The first version shows one month at a time (consistent with how PeriodSelector already works in calendar-accurate mode).

Design constraints

  • dueDay is optional everywhere. Users who don't set due dates still get all existing behavior unchanged. Calendar view degrades gracefully: bills without a dueDay are shown as "unscheduled" in a separate section.
  • No file format migration. dueDay already exists on Bill. Loan gains one new optional field — older plan files simply lack it and default to undefined.
  • The cash flow engine is a pure utility (src/utils/cashFlowProjection.ts) with no React dependencies. It can be tested exhaustively in isolation.

Phase 1: Due-Date Fields on Bills and Loans

Parent status: [ ] Planned

1a. Surface dueDay in the Bill form

  • Add a "Due Day" field to the bill add/edit modal in BillsManager.tsx.
    • Optional number input (1–31). Label: "Day of month due" with helper text "Leave blank if this bill has no fixed due date."
    • For non-monthly frequencies (weekly, bi-weekly), the field label should adapt: "Day of cycle due" or be hidden if it doesn't make sense for the frequency (weekly/bi-weekly have no fixed calendar day). Only show for monthly, quarterly, semi-annual, yearly frequencies.
    • Clamp input to valid range (1–28 for safety, or 1–31 with end-of-month clamping logic matching payCalendar.ts). Use 1–31 with clamping for consistency with existing patterns.
    • Wire dueDay through addBill / updateBill in BudgetContext.
  • Display dueDay on bill cards in BillsManager when set.
    • Subtitle line: "Due on the Xth" or "Due on the Xst/Xnd/Xrd" with ordinal suffix.
  • Add dueDay to bill search module (src/utils/searchModules/) if one exists, so bills can be searched by due date.

1b. Add paymentDueDay to Loan

  • Add paymentDueDay?: number to the Loan interface in src/types/obligations.ts.
    • Same semantics as dueDay on Bill: day of month (1–31), optional.
  • Add "Payment Due Day" field to the loan add/edit modal in LoansManager.tsx.
    • Same UX as the bill field. Only shown for monthly / quarterly / semi-annual / yearly frequencies.
  • Display paymentDueDay prominently on loan cards when set.
  • Wire paymentDueDay through addLoan / updateLoan in BudgetContext.

1c. Tests

  • Unit tests for ordinal suffix formatting utility (1st, 2nd, 3rd, 4th, 11th, 12th, 13th, 21st, …).
  • Verify that plan files without paymentDueDay on loans load without error (backward compat).
  • Verify that bills without dueDay still render identically to current behavior.

Done definition:

  • Both bill and loan forms expose an optional due-day field for monthly+ frequencies.
  • Due day displays on item cards when set.
  • Existing plan files load without migration or data loss.

Phase 2: Cash Flow Projection Engine

Parent status: [ ] Planned

2a. Core utility

  • Create src/utils/cashFlowProjection.ts with:

    • generateBillDueDates(bill, rangeStart, rangeEnd): CashFlowEvent[]
      • Given a bill with dueDay, amount, frequency, and accountId, return all due-date events in the range.
      • For monthly frequency: one event per month on dueDay (clamped to end of month).
      • For quarterly: one event every 3 months.
      • For semi-annual: one event every 6 months.
      • For yearly: one event per year.
      • Bills without dueDay return an empty array (they are "unscheduled").
    • generateLoanPaymentDates(loan, rangeStart, rangeEnd): CashFlowEvent[]
      • Same as above but for loans using paymentDueDay and monthlyPayment (or per-frequency amount from paymentBreakdown).
    • generatePaycheckDeposits(paySettings, accounts, rangeStart, rangeEnd): CashFlowEvent[]
      • Uses existing generatePaycheckDates() from payCalendar.ts.
      • Each paycheck date produces deposit events for each account based on the stable allocation amounts (from accountAllocation.ts).
    • projectCashFlow(events, startingBalances?): CashFlowLedger
      • Takes all events sorted chronologically, applies them to per-account running balances, and returns a ledger.
      • startingBalances is an optional Record<accountId, number> for initial account balances (can default to 0).
      • The ledger is a day-by-day array of { date, events[], balances: Record<accountId, number> }.
    • findNegativeBalanceDays(ledger): NegativeBalanceWarning[]
      • Scans the ledger and returns dates + accounts where the running balance goes negative.
  • Define types in src/types/cashFlow.ts:

    CashFlowEvent { date, accountId, amount (positive=deposit, negative=withdrawal), label, sourceType ('paycheck'|'bill'|'loan'), sourceId }
    CashFlowLedger { days: CashFlowDay[] }
    CashFlowDay { date, events: CashFlowEvent[], balances: Record<string, number> }
    NegativeBalanceWarning { date, accountId, balance, triggeringEvent }
    

2b. Tests

  • cashFlowProjection.test.ts:
    • Monthly bill with dueDay: 15, range = one month → exactly one event on the 15th.
    • Quarterly bill with dueDay: 1, range = 6 months → exactly 2 events.
    • Bill without dueDay → returns empty array.
    • dueDay: 31 in February → clamped to 28 (or 29 in leap year).
    • Paycheck deposits land on correct dates per payCalendar.ts.
    • Running balance goes negative when $1,200 bill hits on the 1st but first paycheck isn't until the 5th.
    • Running balance stays positive when starting balance is seeded with the recommended buffer.
    • Multiple accounts: events correctly isolated per account.
    • Year boundary: range crossing Dec→Jan produces correct events for both months.

Done definition:

  • All utility functions are pure, have no React dependencies, and are exhaustively tested.
  • projectCashFlow produces correct running balances for a known bi-weekly paycheck + monthly bill scenario that can be verified by hand.

Phase 3: Calendar Tab View

Parent status: [ ] Planned

3a. Tab registration

  • Add 'calendar' to TabId union in src/utils/tabManagement.ts.
  • Add 'calendar' to VALID_TABS in PlanDashboard.tsx.
  • Register the tab with icon (CalendarDays from lucide), label "Calendar", and default position (after Savings, or at end).
  • The calendar tab is hidden by default for existing plans (opt-in via Tab Management modal). New plans created after this version include it.

3b. Calendar view component

  • Create src/components/tabViews/CalendarView/CalendarView.tsx:

    • Month grid: standard 7-column calendar grid (Sun–Sat) for the selected month.
    • Period navigation: reuse PeriodSelector pattern from Phase 3 of v0.5.0 (month/year nav with prev/next arrows and "Today" button).
    • Day cells: each cell shows:
      • Paycheck deposits (green badge with amount).
      • Bill/loan withdrawals (red badge with amount).
      • Net balance indicator (green/red dot if balance is positive/negative after all events for that day).
    • Day detail panel: clicking a day cell opens a side panel or expandable section showing:
      • All events for that day (paycheck deposits, bill withdrawals, loan payments).
      • Running balance per account after that day's events.
    • Unscheduled section: below the calendar grid, show bills/loans that lack a dueDay/paymentDueDay in a separate "Unscheduled Obligations" list so the user knows these aren't plotted.
    • Account filter: optional dropdown to filter the view to a single account.
    • Negative balance warnings: prominently highlight cells where any account goes negative. Show a summary alert at the top if any negative days exist: "Account X goes negative on March 3rd — consider adjusting your buffer or moving bill due dates."
  • Create src/components/tabViews/CalendarView/CalendarView.css with component-scoped styles.

  • Create src/components/tabViews/CalendarView/index.ts barrel export.

3c. Integration with existing features

  • Wire calendarAccurate and paychecksInPeriod state from PlanDashboard into CalendarView (so paychecks appear even without calendar-accurate mode — the calendar always uses real dates).
  • The calendar view requires firstPaycheckDate to function. If missing, show an empty state directing the user to Pay Settings (same pattern as Phase 3 of v0.5.0).
  • Use calculateStableAllocation from accountAllocation.ts to determine per-paycheck deposit amounts per account.
  • Cross-link: clicking a bill event in the calendar navigates to that bill in BillsManager (via existing onNavigateToBills pattern). Same for loans.

3d. Tests

  • Snapshot or smoke test for CalendarView rendering with sample budget data.
  • Verify empty state when firstPaycheckDate is missing.
  • Verify "unscheduled" section appears for bills without dueDay.
  • Verify negative balance warning appears when expected.

Done definition:

  • Calendar tab appears in tab list (opt-in for existing plans, default for new plans).
  • Monthly grid correctly plots paycheck and bill events on the right days.
  • Clicking a day shows event details.
  • Negative balance warnings surface correctly.
  • Bills/loans without due dates appear in the unscheduled section, not silently dropped.

Effort Estimate & Risk Analysis

Scope summary

Phase New files Modified files Estimated complexity
1 — Due-date fields 0–1 (ordinal util) 4–5 (types, BillsManager, LoansManager, BudgetContext, search) Low–Medium
2 — Cash flow engine 2 (cashFlowProjection.ts, cashFlow types) 0 Medium
3 — Calendar view 3+ (CalendarView component, CSS, index) 3–4 (tabManagement, PlanDashboard, tab registration) High

Risk areas

  • Non-monthly bill frequencies and due dates: Weekly and bi-weekly bills don't have a natural "day of month." Phase 1 simply hides the due-day field for those frequencies. Phase 2's generateBillDueDates only generates events for monthly+ bills. This is an intentional scope cut — the vast majority of household bills are monthly.
  • Calendar view complexity: The calendar grid + day detail panel + account filtering + negative balance highlighting is the largest single UI deliverable. It should be built incrementally — grid first, then events, then detail panel, then warnings.
  • Performance: projectCashFlow scans one month at a time for display but could be asked for multi-month ranges. Keep it lazy (compute only the displayed month). Memoize with useMemo keyed on [selectedMonth, bills, loans, paySettings, accounts].
  • Interaction with calendar-accurate mode: The calendar view always uses real dates (it is the calendar). The existing calendarAccurate toggle on other views is independent. No conflict, but documentation should clarify the relationship.
  • Backward compatibility: dueDay on Bill already exists and is optional. paymentDueDay on Loan is a new optional field. No migration needed. Older plan files load fine with both fields as undefined.

Phase ordering

Phases are sequential and each is independently shippable:

  • Phase 1 can ship alone — users see due dates on their bills/loans even without the calendar view.
  • Phase 2 can ship with Phase 1 — the engine runs in tests and powers the buffer recommendations, even if the calendar UI isn't built yet.
  • Phase 3 requires Phases 1 and 2 — it's the visualization layer on top.

Final Exit Checklist

  • All parent items above are marked [x] Done or explicitly deferred.
  • RELEASE_NOTES.md updated with completed user-facing items.
  • Lint, typecheck, tests, and build pass for merged changes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions