import { useId } from 'react';
import { motion } from 'framer-motion';
import { Wallet, CalendarClock, Sparkles, ArrowRight } from 'lucide-react';
import { cn, fmt, fmtDate } from '@/lib/utils';
import { asDollars } from '@/lib/money';
import {
buildTimelineGeometry,
daysUntilLabel,
shortDate,
splitUpcoming,
} from '@/lib/cashflowUtils';
import type {
TrackerCashflow,
CashflowTimelinePoint,
SafeToSpendUpcoming,
TimelineBill,
} from '@/types';
const CHART_W = 400;
const CHART_H = 96;
function TimelineChart({
timeline,
positive,
}: {
timeline: CashflowTimelinePoint[];
positive: boolean;
}) {
const gradId = useId();
const geo = buildTimelineGeometry(timeline, CHART_W, CHART_H);
if (!geo) return null;
const tone = positive ? '#10b981' : '#f43f5e'; // emerald-500 / rose-500
return (
{/* zero line — only meaningful when the projection dips negative */}
{geo.points.some((p) => p.balance < 0) && (
)}
{/* bill-day markers */}
{geo.points
.filter((p) => p.isDrop)
.map((p) => (
{`${fmtDate(p.date)} — ${(p.bills as TimelineBill[]).map((b) => `${b.name} ${fmt(b.amount)}`).join(', ')} → ${fmt(asDollars(p.balance))} left`}
))}
{/* payday marker */}
{geo.points
.filter((p) => p.isPayday)
.map((p) => (
{`Payday ${fmtDate(p.date)} — ${fmt(asDollars(p.balance))} left`}
))}
);
}
function UpcomingList({ upcoming }: { upcoming: SafeToSpendUpcoming[] }) {
const { visible, overflow } = splitUpcoming(upcoming, 4);
if (visible.length === 0) {
return (
All bills covered
Nothing else is due before payday.
);
}
return (
{visible.map((bill) => (
{bill.name}
{bill.status === 'late' || bill.status === 'missed'
? 'overdue'
: shortDate(bill.due_date)}
{fmt(bill.amount)}
))}
{overflow > 0 && (
+{overflow} more before payday
)}
);
}
/**
* Safe-to-Spend hero card: what's left for the current 1st/15th period after
* everything still due before the next payday is covered. Sits directly under
* the summary cards and reads from the tracker payload's `cashflow` block.
*/
export default function CashFlowCard({
cashflow,
onSetStartingAmounts,
}: {
cashflow?: TrackerCashflow | null;
onSetStartingAmounts?: () => void;
}) {
if (!cashflow) return null;
// No starting amounts and no bank sync — invite setup instead of guessing.
if (!cashflow.has_data) {
return (
See your safe-to-spend
Add what's in your account for the 1st and 15th, and Bill Tracker projects what's
left after bills.
Set starting amounts
);
}
const safe = Number(cashflow.safe_to_spend ?? 0);
const positive = safe >= 0;
return (
{/* ── Number ── */}
{positive ? '' : '−'}
{fmt(asDollars(Math.abs(safe)))}
until {shortDate(cashflow.next_payday)} · {daysUntilLabel(cashflow.days_until_payday)}
{/* ── Projection ── */}
{fmt(cashflow.available)} on hand →{' '}
{cashflow.still_due_count === 0
? 'no bills left before payday'
: `${cashflow.still_due_count} bill${cashflow.still_due_count === 1 ? '' : 's'} (${fmt(cashflow.still_due_total)}) before payday`}
{/* ── Upcoming ── */}
);
}