81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
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'
|
|
|
|
type SearchParams = Promise<Record<string, string | string[] | undefined>>
|
|
|
|
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 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({
|
|
where: { userId },
|
|
orderBy: { createdAt: 'asc' },
|
|
}),
|
|
prisma.$queryRaw<{ budgetId: string; total: bigint }[]>`
|
|
SELECT t."budgetId",
|
|
COALESCE(SUM(
|
|
CASE
|
|
WHEN a.type = 'CREDIT_CARD' AND t.type = 'CREDIT' THEN t."amountCents"
|
|
WHEN a.type = 'CREDIT_CARD' AND t.type = 'DEBIT' THEN -t."amountCents"
|
|
WHEN t.type = 'DEBIT' THEN t."amountCents"
|
|
ELSE -t."amountCents"
|
|
END
|
|
), 0)::bigint AS total
|
|
FROM "Transaction" t
|
|
JOIN "Account" a ON t."accountId" = a.id
|
|
WHERE a."userId" = ${userId}
|
|
AND t."budgetId" IS NOT NULL
|
|
AND t.date >= ${start}
|
|
AND t.date <= ${end}
|
|
GROUP BY t."budgetId"
|
|
`,
|
|
prisma.budgetRule.findMany({
|
|
where: { userId },
|
|
orderBy: { createdAt: 'asc' },
|
|
select: { id: true, budgetId: true, pattern: true },
|
|
}),
|
|
])
|
|
|
|
const spendMap = new Map(spendRows.map((r) => [r.budgetId, Number(r.total)]))
|
|
const rulesMap = new Map<string, { id: string; pattern: string }[]>()
|
|
for (const rule of rules) {
|
|
const existing = rulesMap.get(rule.budgetId) ?? []
|
|
existing.push({ id: rule.id, pattern: rule.pattern })
|
|
rulesMap.set(rule.budgetId, existing)
|
|
}
|
|
|
|
const budgetsWithSpend = budgets.map((b) => ({
|
|
id: b.id,
|
|
name: b.name,
|
|
limitCents: b.limitCents,
|
|
color: b.color,
|
|
isActive: b.isActive,
|
|
spendCents: spendMap.get(b.id) ?? 0,
|
|
rules: rulesMap.get(b.id) ?? [],
|
|
}))
|
|
|
|
return (
|
|
<div className="p-6 space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold">Budgets</h1>
|
|
<MonthYearPicker />
|
|
</div>
|
|
<BudgetList budgets={budgetsWithSpend} />
|
|
</div>
|
|
)
|
|
}
|