import { Card, Title, Text, Group, Badge, Tooltip } from '@mantine/core'; import { useMemo } from 'react'; const fmt = (v: number) => v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }); const typeColors: Record = { cd: '#228be6', money_market: '#40c057', treasury: '#7950f2', savings: '#fd7e14', other: '#868e96', }; interface Props { investments: any[]; /** Optional shared time range to align with ProjectionChart */ sharedStartDate?: Date; sharedEndDate?: Date; } export function InvestmentTimeline({ investments, sharedStartDate, sharedEndDate }: Props) { const { items, startDate, endDate, totalMonths } = useMemo(() => { const now = new Date(); const items = investments .filter((inv: any) => inv.purchase_date || inv.maturity_date) .map((inv: any) => ({ ...inv, start: inv.purchase_date ? new Date(inv.purchase_date) : now, end: inv.maturity_date ? new Date(inv.maturity_date) : null, })); if (!items.length) return { items: [], startDate: now, endDate: now, totalMonths: 1 }; // Use shared range if provided (to align with ProjectionChart), otherwise compute from investments let startDate: Date; let endDate: Date; if (sharedStartDate && sharedEndDate) { startDate = sharedStartDate; endDate = sharedEndDate; } else { const allDates = items.flatMap((i: any) => [i.start, i.end].filter(Boolean)) as Date[]; startDate = new Date(Math.min(...allDates.map((d) => d.getTime()))); endDate = new Date(Math.max(...allDates.map((d) => d.getTime()))); } const totalMonths = Math.max( (endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth()) + 1, 1, ); return { items, startDate, endDate, totalMonths }; }, [investments, sharedStartDate, sharedEndDate]); if (!items.length) return null; const getPercent = (date: Date) => { const months = (date.getFullYear() - startDate.getFullYear()) * 12 + (date.getMonth() - startDate.getMonth()); return Math.max(0, Math.min(100, (months / totalMonths) * 100)); }; // Generate year labels const yearLabels: { year: number; percent: number }[] = []; for (let y = startDate.getFullYear(); y <= endDate.getFullYear(); y++) { const janDate = new Date(y, 0, 1); if (janDate >= startDate && janDate <= endDate) { yearLabels.push({ year: y, percent: getPercent(janDate) }); } } return ( Investment Timeline {/* Year markers */}
{yearLabels.map((yl) => ( {yl.year} ))}
{/* Timeline bars */}
{/* Background grid */}
{yearLabels.map((yl) => (
))}
{items.map((inv: any, idx: number) => { const leftPct = getPercent(inv.start); const rightPct = inv.end ? getPercent(inv.end) : leftPct + 2; const widthPct = Math.max(rightPct - leftPct, 1); const color = typeColors[inv.investment_type] || '#868e96'; return ( {inv.label} {fmt(parseFloat(inv.principal))} @ {parseFloat(inv.interest_rate || 0).toFixed(2)}% {inv.purchase_date && Start: {new Date(inv.purchase_date).toLocaleDateString()}} {inv.maturity_date && Maturity: {new Date(inv.maturity_date).toLocaleDateString()}}
} position="top" multiline withArrow >
{inv.label} — {fmt(parseFloat(inv.principal))}
); })}
{/* Legend */} {Object.entries(typeColors).map(([type, color]) => (
{type.replace('_', ' ')} ))} ); }