import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
ChartContainer,
ChartTooltip,
type ChartConfig,
} from '@/components/ui/chart';
import { useLocale } from '@/hooks/use-locale';
import { BudgetPeriod } from '@/types/budget';
import { formatCurrency } from '@/utils/currency';
import { formatDate } from '@/utils/date';
import { __ } from '@/utils/i18n';
import { useMemo } from 'react';
import { Area, AreaChart, Line, XAxis } from 'recharts';
interface Props {
currentPeriod: BudgetPeriod;
previousPeriod?: BudgetPeriod | null;
budgetName: string;
currencyCode: string;
}
interface ChartDataPoint {
day: number;
date: string;
spent: number;
allocated: number;
remaining: number;
prevSpent?: number;
prevDate?: string;
}
interface CustomTooltipProps {
active?: boolean;
payload?: Array<{
payload: ChartDataPoint;
}>;
label?: string | number;
currencyCode: string;
hasPreviousPeriod: boolean;
locale: string;
}
function CustomTooltip({
active,
payload,
currencyCode,
hasPreviousPeriod,
locale,
}: CustomTooltipProps) {
if (!active || !payload || !payload.length) {
return null;
}
const data = payload[0].payload;
const allocated = data.allocated;
const spent = data.spent;
const available = data.remaining;
const percentage =
allocated > 0 ? Math.round((available / allocated) * 100) : 0;
return (
{hasPreviousPeriod
? `Day ${data.day}`
: formatDate(data.date, 'MMM d, yyyy', locale)}
{__('Allocated:')}
{formatCurrency(allocated, currencyCode)}
{__('Spent:')}
{formatCurrency(spent, currencyCode)}
{hasPreviousPeriod && data.prevSpent !== undefined && (
{__('Last period:')}
{formatCurrency(data.prevSpent, currencyCode)}
)}
{__('Available:')}
{percentage}% /
{formatCurrency(available, currencyCode)}
);
}
function buildCumulativeSpending(period: BudgetPeriod): Map {
const transactions = period.budget_transactions || [];
const transactionsByDate = new Map();
transactions.forEach((t) => {
if (!t.transaction) return;
const date = new Date(t.transaction.transaction_date)
.toISOString()
.split('T')[0];
transactionsByDate.set(
date,
(transactionsByDate.get(date) || 0) + t.amount,
);
});
return transactionsByDate;
}
export function BudgetSpendingChart({
currentPeriod,
previousPeriod,
budgetName,
currencyCode,
}: Props) {
const locale = useLocale();
const hasPreviousPeriod = !!previousPeriod;
const chartData = useMemo(() => {
const currentByDate = buildCumulativeSpending(currentPeriod);
const prevByDate = previousPeriod
? buildCumulativeSpending(previousPeriod)
: null;
const startDate = new Date(currentPeriod.start_date);
const endDate = new Date(currentPeriod.end_date);
const prevStartDate = previousPeriod
? new Date(previousPeriod.start_date)
: null;
const prevEndDate = previousPeriod
? new Date(previousPeriod.end_date)
: null;
const data: ChartDataPoint[] = [];
let cumulativeSpent = 0;
let prevCumulativeSpent = 0;
const currentDate = new Date(startDate);
let dayIndex = 1;
while (currentDate <= endDate) {
const dateStr = currentDate.toISOString().split('T')[0];
const dailySpent = currentByDate.get(dateStr) || 0;
cumulativeSpent += dailySpent;
const point: ChartDataPoint = {
day: dayIndex,
date: dateStr,
spent: cumulativeSpent,
allocated: currentPeriod.allocated_amount,
remaining: currentPeriod.allocated_amount - cumulativeSpent,
};
// Map to the same day index in the previous period
if (prevByDate && prevStartDate && prevEndDate) {
const prevDate = new Date(prevStartDate);
prevDate.setDate(prevDate.getDate() + dayIndex - 1);
if (prevDate <= prevEndDate) {
const prevDateStr = prevDate.toISOString().split('T')[0];
const prevDailySpent = prevByDate.get(prevDateStr) || 0;
prevCumulativeSpent += prevDailySpent;
point.prevSpent = prevCumulativeSpent;
point.prevDate = prevDateStr;
}
}
data.push(point);
currentDate.setDate(currentDate.getDate() + 1);
dayIndex++;
}
return data;
}, [currentPeriod, previousPeriod]);
const chartConfig = {
spent: {
label: 'Spent',
color: 'var(--spent)',
},
allocated: {
label: 'Budget',
color: 'var(--allocated)',
},
...(hasPreviousPeriod && {
prevSpent: {
label: 'Last Period',
color: 'var(--spent-prev)',
},
}),
} satisfies ChartConfig;
const periodLabel = useMemo(() => {
const start = formatDate(currentPeriod.start_date, 'MMM d', locale);
const end = formatDate(currentPeriod.end_date, 'MMM d, yyyy', locale);
return `${start} - ${end}`;
}, [currentPeriod, locale]);
return (
{__('Budget Spending')}
{__('Tracking spending for')}
{budgetName} ยท {periodLabel}
{
if (hasPreviousPeriod) {
return `Day ${value}`;
}
return formatDate(value, 'MMM d', locale);
}}
/>
}
/>
{hasPreviousPeriod && (
)}
);
}