import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { toast } from 'sonner'; import { CalendarDays, CheckCircle2, ChevronLeft, ChevronRight, Edit3, Loader2, Minus, Printer, RotateCcw, Save, } from 'lucide-react'; import { api } from '@/api.js'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { cn, fmt } from '@/lib/utils'; const MONTHS = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; function selectedFromToday() { const now = new Date(); return { year: now.getFullYear(), month: now.getMonth() + 1 }; } function shiftMonth(year, month, delta) { const next = new Date(year, month - 1 + delta, 1); return { year: next.getFullYear(), month: next.getMonth() + 1 }; } function monthLabel(year, month) { return `${MONTHS[month - 1]} ${year}`; } function moneyClass(value) { return value >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive'; } function StatusMark({ expense }) { if (expense.is_skipped) { return ( Skipped ); } if (expense.is_paid) { return ( Paid ); } return ( Open ); } function SummaryChart({ rows = [] }) { const max = Math.max(1, ...rows.map(row => Math.abs(Number(row.amount) || 0))); const chartRows = rows.map((row, index) => ({ ...row, label: row.type === 'Remaining' ? Number(row.amount) >= 0 ? 'Remaining' : 'Shortfall' : row.type, color: index === 0 ? 'hsl(var(--chart-1))' : index === 1 ? 'hsl(var(--chart-3))' : Number(row.amount) >= 0 ? 'hsl(var(--chart-2))' : 'hsl(var(--destructive))', width: Math.max(2, (Math.abs(Number(row.amount) || 0) / max) * 100), })); return (
{chartRows.map(row => (
{row.label}
{fmt(row.amount)}
))}
); } function ExpenseRow({ expense }) { return (
{expense.name}
{expense.category_name && {expense.category_name}} Due day {expense.due_day} {expense.actual_amount !== null && Monthly amount}
{fmt(expense.display_amount)}
); } export default function SummaryPage() { const [selected, setSelected] = useState(selectedFromToday); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [startingFirst, setStartingFirst] = useState('0'); const [startingFifteenth, setStartingFifteenth] = useState('0'); const [startingOther, setStartingOther] = useState('0'); const [editingStarting, setEditingStarting] = useState(false); const loadSummary = useCallback(async () => { setLoading(true); setError(''); try { const result = await api.summary(selected.year, selected.month); setData(result); setStartingFirst(String(result.starting_amounts?.first_amount ?? 0)); setStartingFifteenth(String(result.starting_amounts?.fifteenth_amount ?? 0)); setStartingOther(String(result.starting_amounts?.other_amount ?? 0)); setEditingStarting(false); } catch (err) { setError(err.message || 'Summary could not be loaded.'); toast.error(err.message || 'Summary could not be loaded.'); } finally { setLoading(false); } }, [selected.month, selected.year]); useEffect(() => { loadSummary(); }, [loadSummary]); const summary = data?.summary || {}; const expenses = data?.expenses || []; const starting = data?.starting_amounts || {}; const generatedLabel = useMemo(() => { if (!data?.generated_at) return ''; return new Date(data.generated_at).toLocaleString(); }, [data?.generated_at]); async function saveStartingAmounts() { const first = Number(startingFirst); const fifteenth = Number(startingFifteenth); const other = Number(startingOther); if (![first, fifteenth, other].every(value => Number.isFinite(value) && value >= 0)) { toast.error('Enter non-negative starting amounts.'); return; } setSaving(true); try { await api.updateMonthlyStartingAmounts({ year: selected.year, month: selected.month, first_amount: first, fifteenth_amount: fifteenth, other_amount: other, }); toast.success('Starting amounts saved.'); await loadSummary(); } catch (err) { toast.error(err.message || 'Starting amounts could not be saved.'); } finally { setSaving(false); } } function moveMonth(delta) { setSelected(current => shiftMonth(current.year, current.month, delta)); } function resetToday() { setSelected(selectedFromToday()); } return (

BillTracker Summary

{monthLabel(selected.year, selected.month)}

{generatedLabel &&

Generated {generatedLabel}

}

Summary

Plan starting balance, expenses, and monthly result.

{monthLabel(selected.year, selected.month)}
{loading && ( Loading summary... )} {!loading && error && (

{error}

)} {!loading && !error && data && ( <> Monthly Plan {monthLabel(data.year, data.month)}

Starting Balance

1st
{fmt(starting.first_amount)}
15th
{fmt(starting.fifteenth_amount)}
Other
{fmt(starting.other_amount)}
Total starting
{fmt(starting.combined_amount)}
Paid
{fmt(starting.paid_total)}
Total remaining
{fmt(starting.combined_remaining)}
{data.previous_month && (
Previous month remaining: {fmt(data.previous_month.combined_remaining)}
)} {editingStarting && (
)}

Expenses

Skipped bills are shown but not counted.

Paid
{expenses.length === 0 ? (
No bills found for this month.
) : (
{expenses.map(expense => ( ))}
)}
Fully Paid Expenses
{summary.paid_expense_count || 0} / {summary.expense_count || 0}
Expenses
{fmt(summary.expense_total)}
Result
{fmt(summary.result)}
Total amount per type Starting balance, planned expenses, and {Number(summary.result || 0) >= 0 ? 'remaining' : 'shortfall'} for {monthLabel(data.year, data.month)}.
Generated {generatedLabel || 'now'}
)}
); }