"use client"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { getCategoryLabel } from "@/lib/categories"; import { getCurrentMonthKey, getMonthLabel } from "@/lib/date"; import { formatCurrencyFromCents, formatPercent } from "@/lib/money"; type DashboardSnapshot = { month: string; insight: { summary: string; recommendations: string; generatedAt: string; } | null; totals: { expensesCents: number; paychecksCents: number; netCashFlowCents: number; averageDailySpendCents: number; paycheckCoverageRatio: number | null; }; comparisons: { highestCategory: { category: string; amountCents: number } | null; largestExpense: { title: string; amountCents: number; date: string; category: string } | null; }; categoryBreakdown: Array<{ category: string; amountCents: number }>; recentExpenses: Array<{ id: string; title: string; amountCents: number; date: string; category: string }>; chart: Array<{ date: string; expensesCents: number; paychecksCents: number }>; }; type OllamaStatus = { available: boolean; configuredModel: string; configuredUrl: string; installedModels: string[]; modelReady: boolean; message: string; }; export function HomeDashboard() { const [selectedMonth, setSelectedMonth] = useState(getCurrentMonthKey()); const [snapshot, setSnapshot] = useState(null); const [error, setError] = useState(null); const [insightBusy, setInsightBusy] = useState(false); const [ollamaBusy, setOllamaBusy] = useState(false); const [ollamaStatus, setOllamaStatus] = useState(null); async function loadDashboard(month: string) { const response = await fetch(`/dashboard?month=${month}`, { cache: "no-store" }); const payload = (await response.json()) as DashboardSnapshot & { error?: string }; if (!response.ok) { setError(payload.error ?? "Could not load the dashboard."); return; } setError(null); setSnapshot(payload); } async function loadOllamaStatus() { const response = await fetch("/ollama/status", { cache: "no-store" }); const payload = (await response.json()) as OllamaStatus; setOllamaStatus(payload); } useEffect(() => { const timeoutId = window.setTimeout(() => { void loadDashboard(selectedMonth); }, 0); return () => window.clearTimeout(timeoutId); }, [selectedMonth]); useEffect(() => { const timeoutId = window.setTimeout(async () => { await loadOllamaStatus(); }, 0); return () => window.clearTimeout(timeoutId); }, []); const topCategoryLabel = useMemo(() => { if (!snapshot?.comparisons.highestCategory) { return "No category leader yet"; } return `${getCategoryLabel(snapshot.comparisons.highestCategory.category as never)} · ${formatCurrencyFromCents(snapshot.comparisons.highestCategory.amountCents)}`; }, [snapshot]); const coverageLabel = snapshot?.totals.paycheckCoverageRatio == null ? "No spend yet" : formatPercent(snapshot.totals.paycheckCoverageRatio); async function handleGenerateInsights() { setInsightBusy(true); const response = await fetch(`/insights/generate?month=${selectedMonth}`, { method: "POST" }); const payload = (await response.json().catch(() => null)) as { error?: string } | null; setInsightBusy(false); if (!response.ok) { setError(payload?.error ?? "Could not generate insights."); return; } await loadDashboard(selectedMonth); } async function handlePullModel() { setOllamaBusy(true); const response = await fetch("/ollama/pull", { method: "POST" }); const payload = (await response.json().catch(() => null)) as { error?: string; message?: string } | null; setOllamaBusy(false); if (!response.ok) { setError(payload?.error ?? "Could not pull the configured model."); return; } setError(payload?.message ?? null); await loadOllamaStatus(); } return (

Monthly Expense Tracker

A calm local-first home for everyday spending.

Track expenses and paycheck timing together so the month-to-date picture stays honest.

Add an expense Track paychecks

Month to date

{snapshot ? getMonthLabel(snapshot.month) : getMonthLabel(selectedMonth)}

setSelectedMonth(event.target.value)} className="rounded-2xl border border-stone-300 bg-stone-50 px-3 py-2 text-sm font-medium text-stone-700 outline-none transition focus:border-stone-900" />

Total spent

{formatCurrencyFromCents(snapshot?.totals.expensesCents ?? 0)}

Paychecks tracked

{formatCurrencyFromCents(snapshot?.totals.paychecksCents ?? 0)}

Net cash flow

{formatCurrencyFromCents(snapshot?.totals.netCashFlowCents ?? 0)}

Average daily spend

{formatCurrencyFromCents(snapshot?.totals.averageDailySpendCents ?? 0)}

{error ?

{error}

: null}

Private monthly insight

Offline guidance for this month

Ollama runtime

{ollamaStatus?.message ?? "Checking local runtime status..."}

{ollamaStatus?.available && ollamaStatus?.modelReady ? "Ready" : "Needs attention"}

Model: {ollamaStatus?.configuredModel ?? "-"}

URL: {ollamaStatus?.configuredUrl ?? "-"}

{ollamaStatus?.available && !ollamaStatus.modelReady ? ( ) : null} Download backup
{snapshot?.insight ? (

Summary

{snapshot.insight.summary}

Next month guidance

{snapshot.insight.recommendations}

Generated {new Date(snapshot.insight.generatedAt).toLocaleString()}

) : (
No saved insight for this month yet. Generate one to get a private offline summary.
)}

Comparisons

What stands out this month

Manage expenses

Highest category

{topCategoryLabel}

Paycheck coverage

{coverageLabel}

Largest expense

{snapshot?.comparisons.largestExpense ? (

{snapshot.comparisons.largestExpense.title}

{snapshot.comparisons.largestExpense.date} · {getCategoryLabel(snapshot.comparisons.largestExpense.category as never)}

{formatCurrencyFromCents(snapshot.comparisons.largestExpense.amountCents)}

) : (

No expense data for this month yet.

)}

Category breakdown

Where the month is going

{snapshot?.categoryBreakdown.length ? ( snapshot.categoryBreakdown.map((item) => (

{getCategoryLabel(item.category as never)}

{formatCurrencyFromCents(item.amountCents)}

)) ) : (
No category totals yet for this month.
)}

Recent expense pulse

Latest entries

Manage paychecks
{snapshot?.recentExpenses.length ? ( snapshot.recentExpenses.map((expense) => (

{expense.title}

{expense.date} · {getCategoryLabel(expense.category as never)}

{formatCurrencyFromCents(expense.amountCents)}

)) ) : (
No expenses recorded yet. Start with one quick entry.
)}
); }