From 91a33cdfecc86cf48b0549971f58a41974cea64e Mon Sep 17 00:00:00 2001 From: jerick Date: Mon, 20 Apr 2026 23:31:59 -0400 Subject: [PATCH] Month Year selection for Dashboard --- src/app/(app)/budgets/page.tsx | 25 ++++++-- src/app/(app)/dashboard/page.tsx | 67 +++++++++++++++----- src/components/budgets/BudgetCard.tsx | 2 +- src/components/dashboard/MonthYearPicker.tsx | 52 +++++++++++++++ src/components/dashboard/NetWorthCard.tsx | 4 +- 5 files changed, 126 insertions(+), 24 deletions(-) create mode 100644 src/components/dashboard/MonthYearPicker.tsx diff --git a/src/app/(app)/budgets/page.tsx b/src/app/(app)/budgets/page.tsx index 545076b..2de49f5 100644 --- a/src/app/(app)/budgets/page.tsx +++ b/src/app/(app)/budgets/page.tsx @@ -1,15 +1,24 @@ import { auth } from '@/lib/auth' import { prisma } from '@/lib/prisma' +import { monthBounds } from '@/lib/utils/dates' import { BudgetList } from '@/components/budgets/BudgetList' +import { MonthYearPicker } from '@/components/dashboard/MonthYearPicker' -export default async function BudgetsPage() { +type SearchParams = Promise> + +export default async function BudgetsPage({ searchParams }: { searchParams: SearchParams }) { const session = await auth() if (!session?.user?.id) return null const userId = session.user.id + const sp = await searchParams + const get = (k: string) => (Array.isArray(sp[k]) ? sp[k][0] : sp[k]) ?? '' + const now = new Date() - const monthStart = new Date(now.getFullYear(), now.getMonth(), 1) - const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999) + const month = Number(get('month')) || (now.getMonth() + 1) + const year = Number(get('year')) || now.getFullYear() + const selectedDate = new Date(year, month - 1, 1) + const { start, end } = monthBounds(selectedDate) const [budgets, spendRows, rules] = await Promise.all([ prisma.budget.findMany({ @@ -23,8 +32,8 @@ export default async function BudgetsPage() { WHERE a."userId" = ${userId} AND t."budgetId" IS NOT NULL AND t.type = 'DEBIT' - AND t.date >= ${monthStart} - AND t.date <= ${monthEnd} + AND t.date >= ${start} + AND t.date <= ${end} GROUP BY t."budgetId" `, prisma.budgetRule.findMany({ @@ -53,7 +62,11 @@ export default async function BudgetsPage() { })) return ( -
+
+
+

Budgets

+ +
) diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index e1fac7b..3c92947 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -5,20 +5,27 @@ import { NetWorthCard } from '@/components/dashboard/NetWorthCard' import { CashFlowCard } from '@/components/dashboard/CashFlowCard' import { RecentTransactions } from '@/components/dashboard/RecentTransactions' import { BudgetSummary } from '@/components/dashboard/BudgetSummary' +import { MonthYearPicker } from '@/components/dashboard/MonthYearPicker' -export default async function DashboardPage() { +type SearchParams = Promise> + +export default async function DashboardPage({ searchParams }: { searchParams: SearchParams }) { const session = await auth() if (!session?.user?.id) return null const userId = session.user.id - const { start, end } = monthBounds() + const sp = await searchParams + const get = (k: string) => (Array.isArray(sp[k]) ? sp[k][0] : sp[k]) ?? '' - const [accounts, cashFlowRows, budgets, spendRows, recentTx] = await Promise.all([ - prisma.account.findMany({ - where: { userId, isActive: true }, - select: { id: true, name: true, type: true, currentBalanceCents: true }, - orderBy: { name: 'asc' }, - }), + const now = new Date() + const month = Number(get('month')) || (now.getMonth() + 1) + const year = Number(get('year')) || now.getFullYear() + const isCurrentMonth = month === (now.getMonth() + 1) && year === now.getFullYear() + + const selectedDate = new Date(year, month - 1, 1) + const { start, end } = monthBounds(selectedDate) + + const [cashFlowRows, budgets, spendRows, recentTx] = await Promise.all([ prisma.$queryRaw<{ type: string; total: bigint }[]>` SELECT t.type, COALESCE(SUM(t."amountCents"), 0)::bigint AS total FROM "Transaction" t @@ -45,15 +52,45 @@ export default async function DashboardPage() { GROUP BY t."budgetId" `, prisma.transaction.findMany({ - where: { account: { userId } }, + where: { + account: { userId }, + date: { gte: start, lte: end }, + }, include: { account: { select: { name: true } } }, orderBy: { date: 'desc' }, take: 5, }), ]) - const bankAccounts = accounts.filter((a) => a.type === 'BANK') - const netWorthCents = bankAccounts.reduce((s, a) => s + a.currentBalanceCents, 0) + // Net worth: live balances for current month, snapshots for past months + let netWorthCents: number + let netWorthAccounts: { id: string; name: string; balanceCents: number }[] + + if (isCurrentMonth) { + const accounts = await prisma.account.findMany({ + where: { userId, isActive: true, type: 'BANK' }, + select: { id: true, name: true, currentBalanceCents: true }, + orderBy: { name: 'asc' }, + }) + netWorthAccounts = accounts.map((a) => ({ id: a.id, name: a.name, balanceCents: a.currentBalanceCents })) + netWorthCents = netWorthAccounts.reduce((s, a) => s + a.balanceCents, 0) + } else { + const snapshots = await prisma.$queryRaw<{ accountId: string; name: string; balanceCents: bigint }[]>` + SELECT bs."accountId", a.name, bs."balanceCents" + FROM "BalanceSnapshot" bs + JOIN "Account" a ON bs."accountId" = a.id + WHERE a."userId" = ${userId} + AND a.type = 'BANK' + AND bs.year = ${year} + AND bs.month = ${month} + ` + netWorthAccounts = snapshots.map((s) => ({ + id: s.accountId, + name: s.name, + balanceCents: Number(s.balanceCents), + })) + netWorthCents = netWorthAccounts.reduce((s, a) => s + a.balanceCents, 0) + } const cfMap = Object.fromEntries(cashFlowRows.map((r) => [r.type, Number(r.total)])) const creditsCents = cfMap['CREDIT'] ?? 0 @@ -80,15 +117,15 @@ export default async function DashboardPage() { return (
-
+

Dashboard

-

{monthLabel()}

+
- + ) : ( -

This month

+

All time

)} {!budget.isActive && ( diff --git a/src/components/dashboard/MonthYearPicker.tsx b/src/components/dashboard/MonthYearPicker.tsx new file mode 100644 index 0000000..e5f7740 --- /dev/null +++ b/src/components/dashboard/MonthYearPicker.tsx @@ -0,0 +1,52 @@ +'use client' + +import { useSearchParams, useRouter, usePathname } from 'next/navigation' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export function MonthYearPicker() { + const searchParams = useSearchParams() + const router = useRouter() + const pathname = usePathname() + + const now = new Date() + const currentMonth = now.getMonth() + 1 + const currentYear = now.getFullYear() + + const month = Number(searchParams.get('month')) || currentMonth + const year = Number(searchParams.get('year')) || currentYear + const isCurrentMonth = month === currentMonth && year === currentYear + + function navigate(delta: number) { + let m = month + delta + let y = year + if (m < 1) { m = 12; y-- } + if (m > 12) { m = 1; y++ } + const params = new URLSearchParams(searchParams.toString()) + params.set('month', String(m)) + params.set('year', String(y)) + router.push(`${pathname}?${params.toString()}`) + } + + const label = new Date(year, month - 1, 1).toLocaleDateString('en-US', { + month: 'long', year: 'numeric', + }) + + return ( +
+ + {label} + +
+ ) +} diff --git a/src/components/dashboard/NetWorthCard.tsx b/src/components/dashboard/NetWorthCard.tsx index 3ce46dd..8c6dc03 100644 --- a/src/components/dashboard/NetWorthCard.tsx +++ b/src/components/dashboard/NetWorthCard.tsx @@ -4,7 +4,7 @@ import { formatCents } from '@/lib/utils/currency' interface BankAccount { id: string name: string - currentBalanceCents: number + balanceCents: number } interface NetWorthCardProps { @@ -26,7 +26,7 @@ export function NetWorthCard({ netWorthCents, bankAccounts }: NetWorthCardProps)
{a.name} - {formatCents(a.currentBalanceCents)} + {formatCents(a.balanceCents)}
))}