import { useState } from 'react';
import { TrendingUp, TrendingDown, ChevronDown, ChevronUp, EyeOff } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api';
import { cn, fmt } from '@/lib/utils';
import { asDollars } from '@/lib/money';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
import type { DriftBill } from '@/types';
function DriftRow({ row, refresh }: { row: DriftBill; refresh: () => void }) {
const [loading, setLoading] = useState(false);
async function handleUpdate() {
setLoading(true);
const oldAmount = row.expected_amount;
try {
await api.updateBill(row.id, { expected_amount: row.recent_amount });
toast.success(`"${row.name}" updated to ${fmt(row.recent_amount)}`, {
action: {
label: 'Undo',
onClick: async () => {
try {
await api.updateBill(row.id, { expected_amount: oldAmount });
toast.success('Reverted');
refresh();
} catch {
toast.error('Failed to revert');
}
},
},
});
refresh();
} catch {
toast.error('Failed to update bill');
} finally {
setLoading(false);
}
}
async function handleDismiss() {
setLoading(true);
try {
await api.snoozeBillDrift(row.id);
toast.success('Hidden for 30 days');
refresh();
} catch {
toast.error('Failed to dismiss');
} finally {
setLoading(false);
}
}
const isUp = row.direction === 'up';
const sign = isUp ? '+' : '';
return (
{/* Bill info */}
{row.name}
{row.category_name && (
{row.category_name}
)}
{isUp ? (
) : (
)}
{sign}
{row.drift_pct}% over {row.months_sampled} months
{/* Amount change */}
{fmt(row.expected_amount)}
→
{fmt(row.recent_amount)}
{sign}
{row.drift_pct}%
{/* Actions */}
);
}
interface DriftInsightPanelProps {
driftBills?: DriftBill[];
refresh: () => void;
// Controlled + persisted collapse state (falls back to local state when omitted).
collapsed?: boolean;
onCollapsedChange?: (collapsed: boolean) => void;
// Fully hide the panel (writes the tracker_show_* setting on the page).
onHide?: () => void;
}
export default function DriftInsightPanel({
driftBills,
refresh,
collapsed,
onCollapsedChange,
onHide,
}: DriftInsightPanelProps) {
const [localOpen, setLocalOpen] = useState(true);
const isControlled = collapsed !== undefined;
const isOpen = isControlled ? !collapsed : localOpen;
const setOpen = (open: boolean) => {
if (isControlled) onCollapsedChange?.(!open);
else setLocalOpen(open);
};
if (!driftBills?.length) return null;
const totalNetDelta = driftBills.reduce(
(sum, b) => sum + (b.recent_amount - b.expected_amount),
0,
);
const sign = totalNetDelta >= 0 ? '+' : '';
const netColor =
totalNetDelta >= 0 ? 'text-amber-500 dark:text-amber-400' : 'text-teal-500 dark:text-teal-400';
const hasIncrease = driftBills.some((b) => b.direction === 'up');
const ToggleIcon = isOpen ? ChevronDown : ChevronUp;
return (
{/* Header */}
{onHide && (
)}
{/* Bill rows */}
{driftBills.map((row) => (
))}
);
}