From c852ad0d803fd2477acc00e14ab94123bab4378e Mon Sep 17 00:00:00 2001 From: Vijayakanth Manoharan Date: Mon, 23 Mar 2026 22:53:14 -0400 Subject: [PATCH] Fix month-browsable expense history --- .../specs/expense-tracking/spec.md | 7 ++ src/components/expense-workspace.tsx | 66 ++++++++++++++----- src/lib/insights.test.ts | 4 ++ 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/openspec/changes/monthly-expense-tracker-v1/specs/expense-tracking/spec.md b/openspec/changes/monthly-expense-tracker-v1/specs/expense-tracking/spec.md index d265e0d..3d2844f 100644 --- a/openspec/changes/monthly-expense-tracker-v1/specs/expense-tracking/spec.md +++ b/openspec/changes/monthly-expense-tracker-v1/specs/expense-tracking/spec.md @@ -18,6 +18,13 @@ The system SHALL allow the user to list recorded expenses and delete a specific - **WHEN** the user requests expenses for the app - **THEN** the system returns stored expenses in a stable order with their recorded fields +### Requirement: User can browse expense history by month +The system SHALL allow the user to select a `YYYY-MM` month when reviewing expense history and SHALL return the expenses recorded for that month. + +#### Scenario: Prior month entries are visible +- **WHEN** the user selects February 2026 in the add-expense history view +- **THEN** the system shows the expenses recorded in February 2026 and exposes delete actions for deletable entries in that month + #### Scenario: Expense is deleted - **WHEN** the user deletes an existing expense - **THEN** the system removes that expense and it no longer appears in future listings or aggregates diff --git a/src/components/expense-workspace.tsx b/src/components/expense-workspace.tsx index cd48122..ae01dcd 100644 --- a/src/components/expense-workspace.tsx +++ b/src/components/expense-workspace.tsx @@ -1,9 +1,9 @@ "use client"; -import { useEffect, useMemo, useState, type FormEvent } from "react"; +import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react"; import { getCategoryLabel, type CategoryValue } from "@/lib/categories"; -import { getCurrentMonthKey, getLocalToday } from "@/lib/date"; +import { getCurrentMonthKey, getLocalToday, getMonthLabel } from "@/lib/date"; import { formatCurrencyFromCents } from "@/lib/money"; type SuggestionResponse = { @@ -34,6 +34,7 @@ type Props = { export function ExpenseWorkspace({ categoryOptions }: Props) { const [expenses, setExpenses] = useState([]); + const [selectedMonth, setSelectedMonth] = useState(""); const [formState, setFormState] = useState<{ title: string; amount: string; @@ -53,23 +54,33 @@ export function ExpenseWorkspace({ categoryOptions }: Props) { const [lastSuggestedMerchant, setLastSuggestedMerchant] = useState(""); const [suggestedCategory, setSuggestedCategory] = useState(null); - useEffect(() => { - async function loadExpenses() { - const month = getCurrentMonthKey(); - const response = await fetch(`/expenses?month=${month}`, { cache: "no-store" }); - const payload = (await response.json().catch(() => null)) as { expenses?: ExpenseRecord[] } | null; - setExpenses(payload?.expenses ?? []); - } + const loadExpenses = useCallback(async (month: string) => { + const response = await fetch(`/expenses?month=${month}`, { cache: "no-store" }); + const payload = (await response.json().catch(() => null)) as { expenses?: ExpenseRecord[] } | null; + setExpenses(payload?.expenses ?? []); + }, []); + useEffect(() => { const timeoutId = window.setTimeout(() => { + setSelectedMonth(getCurrentMonthKey()); setFormState((current) => (current.date ? current : { ...current, date: getLocalToday() })); }, 0); - void loadExpenses(); - return () => window.clearTimeout(timeoutId); }, []); + useEffect(() => { + if (!selectedMonth) { + return; + } + + const timeoutId = window.setTimeout(() => { + void loadExpenses(selectedMonth); + }, 0); + + return () => window.clearTimeout(timeoutId); + }, [loadExpenses, selectedMonth]); + const totalSpent = useMemo( () => expenses.reduce((sum, expense) => sum + expense.amountCents, 0), [expenses], @@ -188,6 +199,10 @@ export function ExpenseWorkspace({ categoryOptions }: Props) { setExpenses((current) => [payload.expense, ...current]); } + if (selectedMonth) { + await loadExpenses(selectedMonth); + } + setFormState((current) => ({ ...current, title: "", amount: "" })); setSuggestionMessage(null); setNeedsSuggestionConfirmation(false); @@ -208,6 +223,11 @@ export function ExpenseWorkspace({ categoryOptions }: Props) { return; } + if (selectedMonth) { + await loadExpenses(selectedMonth); + return; + } + setExpenses((current) => current.filter((expense) => expense.id !== id)); } @@ -327,15 +347,31 @@ export function ExpenseWorkspace({ categoryOptions }: Props) {
-
-

Recent entries

-

Expense history

+
+
+

Recent entries

+

Expense history

+ {selectedMonth ? ( +

+ Showing {getMonthLabel(selectedMonth)} entries. +

+ ) : null} +
+
{expenses.length === 0 ? (
- No expenses yet. Add your first entry to start the month. + {selectedMonth ? `No expenses recorded for ${getMonthLabel(selectedMonth)} yet.` : "No expenses yet. Add your first entry to start the month."}
) : ( expenses.map((expense) => ( diff --git a/src/lib/insights.test.ts b/src/lib/insights.test.ts index 2acfb87..b786eaa 100644 --- a/src/lib/insights.test.ts +++ b/src/lib/insights.test.ts @@ -18,6 +18,7 @@ vi.mock("@/lib/db", () => { db: { expense: { findMany: vi.fn() }, paycheck: { findMany: vi.fn() }, + recurringExpense: { findMany: vi.fn().mockResolvedValue([]) }, paySchedule: { findFirst: vi.fn().mockResolvedValue(null) }, monthlyInsight, }, @@ -35,6 +36,7 @@ describe("generateMonthlyInsight", () => { vi.mocked(db.expense.findMany).mockResolvedValue([]); vi.mocked(db.paycheck.findMany).mockResolvedValue([]); + vi.mocked(db.recurringExpense.findMany).mockResolvedValue([]); const result = await generateMonthlyInsight("2026-03"); @@ -72,6 +74,7 @@ describe("generateMonthlyInsight", () => { createdAt: new Date("2026-03-01T10:00:00.000Z"), }, ]); + vi.mocked(db.recurringExpense.findMany).mockResolvedValue([]); vi.spyOn(globalThis, "fetch").mockResolvedValue({ ok: true, @@ -120,6 +123,7 @@ describe("generateMonthlyInsight", () => { createdAt: new Date("2026-03-01T10:00:00.000Z"), }, ]); + vi.mocked(db.recurringExpense.findMany).mockResolvedValue([]); vi.spyOn(globalThis, "fetch").mockResolvedValue({ ok: true,