import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { CalendarDays, ChevronLeft, ChevronRight, CircleDollarSign, RefreshCw } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; import { cn, fmt, fmtDate, todayStr } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; const MONTHS = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; function currentMonth() { 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 displayStatus(status) { if (status === 'due_soon') return 'Due'; if (status === 'late') return 'Late'; return status ? status.charAt(0).toUpperCase() + status.slice(1) : 'Due'; } function statusTone(status) { if (status === 'paid' || status === 'autodraft') return 'bg-emerald-500/15 text-emerald-500 border-emerald-500/25'; if (status === 'skipped') return 'bg-muted text-muted-foreground border-border'; if (status === 'late' || status === 'missed') return 'bg-destructive/15 text-destructive border-destructive/25'; return 'bg-primary/10 text-primary border-primary/25'; } function LegendItem({ className, label }) { return ( {label} ); } function SummaryProgress({ summary }) { const percent = Number(summary?.paid_percent || 0); return (
Total Expenses Paid
Monthly progress across active, unskipped bills.

{fmt(summary?.paid_total)} / {fmt(summary?.expected_total)}

{fmt(summary?.remaining_total)} remaining

{percent}%

paid

{summary?.bill_count || 0} active bills {summary?.paid_count || 0} paid {!!summary?.skipped_count && {summary.skipped_count} skipped} {!!summary?.missed_count && {summary.missed_count} late or missed}
); } function DayIndicators({ day }) { const summary = day.status_summary; const hasPaid = summary.paid_count > 0; const hasDue = summary.due_count > summary.paid_count + summary.skipped_count + summary.missed_count; const hasSkipped = summary.skipped_count > 0; const hasMissed = summary.missed_count > 0; const paymentOnly = day.payments.length > 0 && day.bills_due.length === 0; return (
{hasPaid && } {(hasDue || paymentOnly) && } {hasSkipped && } {hasMissed && }
); } function CalendarGrid({ data, selectedDate, onSelectDay }) { const firstWeekday = new Date(data.year, data.month - 1, 1).getDay(); const cells = [ ...Array.from({ length: firstWeekday }, (_, index) => ({ type: 'blank', key: `blank-${index}` })), ...data.days.map(day => ({ type: 'day', key: day.date, day })), ]; const today = todayStr(); return (
{WEEKDAYS.map(day => (
{day}
))}
{cells.map(cell => { if (cell.type === 'blank') { return
; } const day = cell.day; const isToday = day.date === today; const isSelected = day.date === selectedDate; const summary = day.status_summary; const hasActivity = day.bills_due.length > 0 || day.payments.length > 0; const isPaidDay = summary.due_count > 0 && summary.paid_count >= summary.due_count - summary.skipped_count; const hasMissed = summary.missed_count > 0; return ( ); })}
); } function DayDetailDialog({ day, open, onOpenChange }) { return ( {day ? fmtDate(day.date) : 'Day details'}

Bills due and payments recorded for this date.

{day && (

Bills Due

{day.bills_due.length === 0 ? (
No bills are due on this day.
) : (
{day.bills_due.map(bill => (

{bill.name}

{bill.category_name || 'Uncategorized'}

{displayStatus(bill.status)}

Expected

{fmt(bill.effective_amount)}

Paid

{fmt(bill.paid_amount)}

Due

{fmtDate(bill.due_date)}

))}
)}

Payments

{day.payments.length === 0 ? (
No payments were recorded on this day.
) : (
{day.payments.map(payment => (

{payment.bill_name}

{payment.method || 'Payment'}

{fmt(payment.amount)}
))}
)}
)}
); } export default function CalendarPage() { const initial = currentMonth(); const [year, setYear] = useState(initial.year); const [month, setMonth] = useState(initial.month); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [selectedDay, setSelectedDay] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const load = useCallback(async () => { setLoading(true); setError(''); try { const result = await api.calendar(year, month); setData(result); setSelectedDay(current => current ? result.days.find(day => day.date === current.date) || null : null); } catch (err) { setError(err.message || 'Calendar data could not be loaded.'); toast.error(err.message || 'Calendar data could not be loaded.'); } finally { setLoading(false); } }, [year, month]); useEffect(() => { load(); }, [load]); const monthLabel = useMemo(() => `${MONTHS[month - 1]} ${year}`, [year, month]); const hasAnyBills = Number(data?.summary?.bill_count || 0) + Number(data?.summary?.skipped_count || 0) > 0; function navigate(delta) { const next = shiftMonth(year, month, delta); setYear(next.year); setMonth(next.month); setSelectedDay(null); setDetailOpen(false); } function goToday() { const next = currentMonth(); setYear(next.year); setMonth(next.month); setSelectedDay(null); setDetailOpen(false); } return (

Monthly Calendar

Calendar

View bills, payments, and monthly progress by date.

{monthLabel}
Today
{loading && ( Loading calendar... )} {!loading && error && (

{error}

)} {!loading && !error && data && ( <> { setSelectedDay(day); setDetailOpen(true); }} /> {!hasAnyBills && (

No bills on this calendar yet.

Add a bill to start seeing due dates and payment progress.

)} )}
Selected Day Tap a date to inspect bills and payments. {selectedDay ? (

{fmtDate(selectedDay.date)}

Due

{fmt(selectedDay.status_summary.total_due)}

Paid

{fmt(selectedDay.status_summary.total_paid)}

) : (

No day selected.

)}
); }