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
{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 (
);
}
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.
)}
);
}