import { useState } from 'react'; import { LayoutGroup } from 'framer-motion'; import { ArrowDown, ArrowUp } from 'lucide-react'; import { cn, fmt } from '@/lib/utils'; import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell, } from '@/components/ui/table'; import { TRACKER_SORT_ASC, TRACKER_SORT_DEFAULT, moveInArray } from '@/lib/trackerUtils'; import { TrackerRow as Row } from '@/components/tracker/TrackerRow'; import { MobileTrackerRow } from '@/components/tracker/MobileTrackerRow'; function SortableHead({ sortKey, activeSortKey, sortDir, onSort, children, className }) { const active = activeSortKey === sortKey; const Icon = sortDir === TRACKER_SORT_ASC ? ArrowUp : ArrowDown; return ( ); } export function TrackerBucket({ label, rows, year, month, refresh, onEditBill, loading, onReorderRows, reorderEnabled, movingBillId, sortKey = TRACKER_SORT_DEFAULT, sortDir = TRACKER_SORT_ASC, onSort, driftedIds = new Set() }) { const [draggingId, setDraggingId] = useState(null); const [dropTargetId, setDropTargetId] = useState(null); // Use actual_amount (if set) as the per-row threshold; exclude skipped rows from totals const activeRows = rows.filter(r => !r.is_skipped); const totalThreshold = activeRows.reduce((s, r) => s + (r.actual_amount ?? r.expected_amount ?? 0), 0); const totalPaid = activeRows.reduce((s, r) => s + (r.total_paid || 0), 0); const totalPaidTowardDue = activeRows.reduce((s, r) => { const threshold = Number(r.actual_amount ?? r.expected_amount ?? 0) || 0; const cappedPaid = Number(r.paid_toward_due); return s + (Number.isFinite(cappedPaid) ? cappedPaid : Math.min(Number(r.total_paid) || 0, threshold)); }, 0); const totalOverpaid = Math.max(totalPaid - totalPaidTowardDue, 0); const totalRemaining = Math.max(totalThreshold - totalPaidTowardDue, 0); const skippedCount = rows.length - activeRows.length; const pct = totalThreshold > 0 ? Math.min((totalPaidTowardDue / totalThreshold) * 100, 100) : 0; const allPaid = pct >= 100; function reorderByIndex(fromIndex, toIndex) { if (!reorderEnabled || fromIndex === toIndex || fromIndex < 0 || toIndex < 0) return; onReorderRows?.(moveInArray(rows, fromIndex, toIndex)); } function dragPropsFor(row, index) { if (!reorderEnabled) return { draggable: false }; return { draggable: true, isDragging: draggingId === row.id, isDropTarget: dropTargetId === row.id && draggingId !== row.id, onDragStart: (event) => { setDraggingId(row.id); event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', String(row.id)); }, onDragEnter: () => { if (draggingId && draggingId !== row.id) setDropTargetId(row.id); }, onDragOver: (event) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; if (draggingId && draggingId !== row.id) setDropTargetId(row.id); }, onDrop: (event) => { event.preventDefault(); const sourceId = Number(event.dataTransfer.getData('text/plain') || draggingId); const fromIndex = rows.findIndex(item => item.id === sourceId); reorderByIndex(fromIndex, index); setDraggingId(null); setDropTargetId(null); }, onDragEnd: () => { setDraggingId(null); setDropTargetId(null); }, }; } function moveControlsFor(row, index) { return { enabled: !!reorderEnabled, moving: movingBillId === row.id, canMoveUp: index > 0, canMoveDown: index < rows.length - 1, onMoveUp: () => reorderByIndex(index, index - 1), onMoveDown: () => reorderByIndex(index, index + 1), }; } return (
{/* Bucket header */}
{label} {skippedCount > 0 && ( ({skippedCount} skipped) )}
{Math.round(pct)}%
{fmt(totalPaidTowardDue)} / {fmt(totalThreshold)} {totalOverpaid > 0 && ( +{fmt(totalOverpaid)} )} {!allPaid && totalRemaining > 0 && ( {fmt(totalRemaining)} left )} {allPaid && ( Done )} {!reorderEnabled && rows.length > 1 && ( Clear filters to reorder )}
{loading ? ( Array.from({ length: 3 }).map((_, i) => (

Expected

Remaining

)) ) : rows.length === 0 ? (
No bills match — try adjusting your filters.
) : ( rows.map((r, i) => ( )) )}
Bill Due Expected Last Month Paid Paid Date Status Notes {loading ? ( Array.from({ length: 5 }).map((_, i) => (
)) ) : rows.length === 0 ? ( No bills match — try adjusting your filters. ) : ( {rows.map((r, i) => ( ))} )}
); }