import { CircleAlert, CircleCheck, TriangleAlert } from 'lucide-react'; import type { ReactNode } from 'react'; import { budgetStatus, whole, type BudgetStatus } from '@/lib/money'; import { cn } from '@/lib/utils'; // Progress bars and rings for "spent against budget". Status always comes // with an icon and a word, never color alone. export const STATUS_TONE: Record = { none: { bar: 'bg-primary/70', text: 'text-muted-foreground', soft: 'bg-muted' }, under: { bar: 'bg-good', text: 'text-good-fg', soft: 'bg-good/12' }, close: { bar: 'bg-warn', text: 'text-warn-fg', soft: 'bg-warn/14' }, over: { bar: 'bg-bad', text: 'text-bad-fg', soft: 'bg-bad/12' }, }; export function ProgressBar({ spent, budget, className, pace, scale }: { spent: number; budget: number; className?: string; pace?: number; scale?: number }) { const status = budgetStatus(spent, budget); // Without a budget, the bar compares categories with each other (scale is the largest). const pct = budget > 0 ? Math.min(100, (spent / budget) * 100) : scale ? Math.max(0, Math.min(100, (spent / scale) * 100)) : 0; return (
{pace !== undefined && budget > 0 && pace > 0 && pace < 1 ? ( // Where spending "should" be by today, if it were spread evenly over the month.
) : null}
); } /** "$42 left", "$18 over" or "no budget", with an icon. */ export function BudgetNote({ spent, budget, className, income }: { spent: number; budget: number; className?: string; income?: boolean }) { if (budget <= 0) return No budget; if (income) { const short = budget - spent; return ( {short <= 0 ? : null} {short <= 0 ? `${whole(-short)} above plan` : `${whole(short)} still to come`} ); } const status = budgetStatus(spent, budget); const Icon = status === 'over' ? CircleAlert : status === 'close' ? TriangleAlert : CircleCheck; return ( {status === 'over' ? `${whole(spent - budget)} over` : `${whole(budget - spent)} left`} ); } /** A ring that fills as the month's budget is spent. */ export function Ring({ spent, budget, size = 168, stroke = 14, children, className }: { spent: number; budget: number; size?: number; stroke?: number; children?: ReactNode; className?: string }) { const status = budgetStatus(spent, budget); const r = (size - stroke) / 2; const circumference = 2 * Math.PI * r; const share = budget > 0 ? Math.min(1, spent / budget) : 0; const color = { none: 'stroke-primary', under: 'stroke-good', close: 'stroke-warn', over: 'stroke-bad' }[status]; return (
{children}
); }