BillTracker/client/components/BillsTableInner.jsx

191 lines
6.4 KiB
JavaScript

import { PenLine, EyeOff, Eye, Clock, Trash2, Zap } from 'lucide-react';
import { cn } from '@/lib/utils';
function ordinal(n) {
const d = Number(n);
if (!d) return '—';
if (d > 3 && d < 21) return `${d}th`;
switch (d % 10) {
case 1: return `${d}st`;
case 2: return `${d}nd`;
case 3: return `${d}rd`;
default: return `${d}th`;
}
}
function hasHistoricalVisibility(bill) {
return !!bill.has_history_ranges || (bill.history_visibility && bill.history_visibility !== 'default');
}
function AprColor({ rate }) {
const cls =
rate >= 25 ? 'text-rose-400' :
rate >= 15 ? 'text-amber-400' :
'text-muted-foreground/60';
return <span className={cn('text-[10px] tabular-nums', cls)}>{rate}% APR</span>;
}
const ALL_ON = {
showCategory: true, showDueDay: true, showAmount: true, showCycle: true,
showApr: true, showBalance: true, showMinPayment: true, showAutopay: true, show2fa: true,
};
function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory }) {
const isDebt = bill.current_balance != null || bill.minimum_payment != null;
const hasHistory = hasHistoricalVisibility(bill);
return (
<div className={cn(
'flex items-center gap-3 px-5 py-3.5 transition-colors',
'hover:bg-accent/20',
!bill.active && 'opacity-60',
)}>
{/* Main info */}
<div className="flex-1 min-w-0 space-y-1.5">
{/* Name + badges */}
<div className="flex flex-wrap items-center gap-1.5">
<button
type="button"
onClick={() => onEdit?.(bill.id)}
className="text-sm font-semibold text-foreground hover:text-primary transition-colors text-left truncate max-w-[240px]"
>
{bill.name}
</button>
{prefs.showCategory && bill.category_name && (
<span className="text-[10px] text-muted-foreground border border-border/50 rounded px-1.5 py-0.5 shrink-0">
{bill.category_name}
</span>
)}
{prefs.showAutopay && !!bill.autopay_enabled && (
<span className="text-[10px] font-semibold px-1.5 py-0.5 rounded bg-emerald-500/15 text-emerald-500 shrink-0">
Autopay
</span>
)}
{prefs.show2fa && !!bill.has_2fa && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-violet-500/15 text-violet-400 shrink-0">
2FA
</span>
)}
{hasHistory && (
<span
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-sky-500/25 bg-sky-500/10 text-sky-500"
title="Historical visibility configured"
>
<Clock className="h-2.5 w-2.5" />
</span>
)}
</div>
{/* Meta row */}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
{prefs.showCycle && <span className="capitalize">{bill.billing_cycle || 'monthly'}</span>}
{prefs.showCycle && prefs.showDueDay && <span className="text-border">·</span>}
{prefs.showDueDay && <span>Due {ordinal(bill.due_day)}</span>}
{prefs.showApr && isDebt && bill.interest_rate != null && (
<>
{(prefs.showCycle || prefs.showDueDay) && <span className="text-border">·</span>}
<AprColor rate={bill.interest_rate} />
</>
)}
{prefs.showBalance && isDebt && bill.current_balance != null && (
<>
{(prefs.showCycle || prefs.showDueDay || (prefs.showApr && bill.interest_rate != null)) && <span className="text-border">·</span>}
<span className="text-[10px] text-muted-foreground/70 tabular-nums">
${Number(bill.current_balance).toLocaleString(undefined, { maximumFractionDigits: 0 })} balance
</span>
</>
)}
</div>
</div>
{/* Amount */}
{prefs.showAmount && (
<div className="text-right shrink-0 hidden sm:block">
<p className="font-mono text-sm font-semibold tabular-nums">
${Number(bill.expected_amount).toFixed(2)}
</p>
{prefs.showMinPayment && bill.minimum_payment != null && (
<p className="text-[10px] text-muted-foreground">
${Number(bill.minimum_payment).toFixed(0)} min
</p>
)}
</div>
)}
{/* Action icons */}
<div className="flex items-center gap-0.5 shrink-0">
<button
type="button"
onClick={() => onEdit?.(bill.id)}
title="Edit"
className="p-1.5 rounded-md text-muted-foreground/40 hover:text-foreground hover:bg-muted/60 transition-colors"
>
<PenLine className="h-3.5 w-3.5" />
</button>
{!bill.active && onHistory && (
<button
type="button"
onClick={() => onHistory?.(bill)}
title="History visibility"
className="p-1.5 rounded-md text-muted-foreground/40 hover:text-sky-400 hover:bg-sky-500/10 transition-colors"
>
<Clock className="h-3.5 w-3.5" />
</button>
)}
<button
type="button"
onClick={() => onToggle?.(bill)}
title={bill.active ? 'Deactivate' : 'Activate'}
className={cn(
'p-1.5 rounded-md transition-colors',
bill.active
? 'text-muted-foreground/40 hover:text-amber-400 hover:bg-amber-500/10'
: 'text-muted-foreground/40 hover:text-emerald-400 hover:bg-emerald-500/10',
)}
>
{bill.active ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
</button>
<button
type="button"
onClick={() => onDelete?.(bill)}
title="Delete"
className="p-1.5 rounded-md text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10 transition-colors"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
);
}
export default function BillsTableInner({ bills, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory }) {
return (
<div className="divide-y divide-border/30">
{bills.map(bill => (
<BillCard
key={bill.id}
bill={bill}
prefs={prefs}
onEdit={onEdit}
onToggle={onToggle}
onDelete={onDelete}
onHistory={onHistory}
/>
))}
</div>
);
}