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)
)}
{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) => (
))
) : 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) => (
))}
)}
);
}