2026-07-03 19:11:05 -05:00
|
|
|
import { Plus, Link2, Pencil, Trash2 } from 'lucide-react';
|
|
|
|
|
import { cn, fmt, fmtDate } from '@/lib/utils';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
refactor(ts): convert bill-modal sub-components to TSX (B10)
The 7 BillModal sub-components + transactionDisplay helper: TemplateSection,
AutopayTrustIndicator, PaymentFormFields, PaymentHistoryList, DebtDetailsSection,
LinkedTransactionsSection, UnmatchDialogs. New @/types.BankTransaction — raw bank
amounts stay in CENTS on the wire, so branded `Cents` (formatCentsUSD, not
formatUSD), the complement to the Dollars brand. BulkUnmatchState typed with
null-safe setState callbacks. typecheck 0, lint 0 errors, build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:03:09 -05:00
|
|
|
import type { Payment } from '@/types';
|
2026-07-03 19:11:05 -05:00
|
|
|
|
refactor(ts): convert bill-modal sub-components to TSX (B10)
The 7 BillModal sub-components + transactionDisplay helper: TemplateSection,
AutopayTrustIndicator, PaymentFormFields, PaymentHistoryList, DebtDetailsSection,
LinkedTransactionsSection, UnmatchDialogs. New @/types.BankTransaction — raw bank
amounts stay in CENTS on the wire, so branded `Cents` (formatCentsUSD, not
formatUSD), the complement to the Dollars brand. BulkUnmatchState typed with
null-safe setState callbacks. typecheck 0, lint 0 errors, build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:03:09 -05:00
|
|
|
function isTransactionLinkedPayment(payment: Payment): boolean {
|
2026-07-03 19:11:05 -05:00
|
|
|
return payment?.payment_source === 'transaction_match' || payment?.transaction_id != null;
|
|
|
|
|
}
|
|
|
|
|
|
refactor(ts): convert bill-modal sub-components to TSX (B10)
The 7 BillModal sub-components + transactionDisplay helper: TemplateSection,
AutopayTrustIndicator, PaymentFormFields, PaymentHistoryList, DebtDetailsSection,
LinkedTransactionsSection, UnmatchDialogs. New @/types.BankTransaction — raw bank
amounts stay in CENTS on the wire, so branded `Cents` (formatCentsUSD, not
formatUSD), the complement to the Dollars brand. BulkUnmatchState typed with
null-safe setState callbacks. typecheck 0, lint 0 errors, build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:03:09 -05:00
|
|
|
function isHistoryOnlyPayment(payment: Payment): boolean {
|
2026-07-03 19:11:05 -05:00
|
|
|
return !!payment?.accounting_excluded;
|
|
|
|
|
}
|
|
|
|
|
|
refactor(ts): convert bill-modal sub-components to TSX (B10)
The 7 BillModal sub-components + transactionDisplay helper: TemplateSection,
AutopayTrustIndicator, PaymentFormFields, PaymentHistoryList, DebtDetailsSection,
LinkedTransactionsSection, UnmatchDialogs. New @/types.BankTransaction — raw bank
amounts stay in CENTS on the wire, so branded `Cents` (formatCentsUSD, not
formatUSD), the complement to the Dollars brand. BulkUnmatchState typed with
null-safe setState callbacks. typecheck 0, lint 0 errors, build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:03:09 -05:00
|
|
|
function paymentSourceLabel(source: string | null | undefined): string {
|
|
|
|
|
const labels: Record<string, string> = {
|
2026-07-03 19:11:05 -05:00
|
|
|
manual: 'Manual',
|
|
|
|
|
file_import: 'File import',
|
|
|
|
|
provider_sync: 'Sync',
|
|
|
|
|
transaction_match: 'Transaction',
|
|
|
|
|
auto_match: 'SimpleFIN',
|
|
|
|
|
};
|
refactor(ts): convert bill-modal sub-components to TSX (B10)
The 7 BillModal sub-components + transactionDisplay helper: TemplateSection,
AutopayTrustIndicator, PaymentFormFields, PaymentHistoryList, DebtDetailsSection,
LinkedTransactionsSection, UnmatchDialogs. New @/types.BankTransaction — raw bank
amounts stay in CENTS on the wire, so branded `Cents` (formatCentsUSD, not
formatUSD), the complement to the Dollars brand. BulkUnmatchState typed with
null-safe setState callbacks. typecheck 0, lint 0 errors, build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:03:09 -05:00
|
|
|
return labels[source ?? ''] || source || 'Manual';
|
2026-07-03 19:11:05 -05:00
|
|
|
}
|
|
|
|
|
|
refactor(ts): convert bill-modal sub-components to TSX (B10)
The 7 BillModal sub-components + transactionDisplay helper: TemplateSection,
AutopayTrustIndicator, PaymentFormFields, PaymentHistoryList, DebtDetailsSection,
LinkedTransactionsSection, UnmatchDialogs. New @/types.BankTransaction — raw bank
amounts stay in CENTS on the wire, so branded `Cents` (formatCentsUSD, not
formatUSD), the complement to the Dollars brand. BulkUnmatchState typed with
null-safe setState callbacks. typecheck 0, lint 0 errors, build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:03:09 -05:00
|
|
|
const PAYMENT_SOURCE_TONES: Record<string, string> = {
|
|
|
|
|
manual: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
|
|
|
|
|
file_import: 'border-sky-500/25 bg-sky-500/10 text-sky-600 dark:text-sky-400',
|
|
|
|
|
provider_sync: 'border-violet-500/25 bg-violet-500/10 text-violet-600 dark:text-violet-400',
|
|
|
|
|
transaction_match: 'border-primary/25 bg-primary/10 text-primary',
|
|
|
|
|
auto_match: 'border-violet-500/25 bg-violet-500/10 text-violet-600 dark:text-violet-400',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function paymentSourceTone(source: string | null | undefined): string {
|
|
|
|
|
return PAYMENT_SOURCE_TONES[source ?? ''] || PAYMENT_SOURCE_TONES.manual!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PaymentHistoryListProps {
|
|
|
|
|
payments: Payment[];
|
|
|
|
|
paymentsLoading?: boolean;
|
|
|
|
|
paymentBusy?: boolean;
|
|
|
|
|
onAdd?: () => void;
|
|
|
|
|
onEdit: (payment: Payment) => void;
|
|
|
|
|
onDelete: (payment: Payment) => void;
|
2026-07-03 19:11:05 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Payment-history list for a bill (edit mode): each recorded payment with its
|
|
|
|
|
// source badge, plus edit/remove actions for manual payments (matched and
|
|
|
|
|
// history-only rows are read-only). Presentational — the parent owns the
|
|
|
|
|
// payment state and the add/edit/delete handlers.
|
|
|
|
|
export default function PaymentHistoryList({
|
|
|
|
|
payments,
|
|
|
|
|
paymentsLoading,
|
|
|
|
|
paymentBusy,
|
|
|
|
|
onAdd,
|
|
|
|
|
onEdit,
|
|
|
|
|
onDelete,
|
refactor(ts): convert bill-modal sub-components to TSX (B10)
The 7 BillModal sub-components + transactionDisplay helper: TemplateSection,
AutopayTrustIndicator, PaymentFormFields, PaymentHistoryList, DebtDetailsSection,
LinkedTransactionsSection, UnmatchDialogs. New @/types.BankTransaction — raw bank
amounts stay in CENTS on the wire, so branded `Cents` (formatCentsUSD, not
formatUSD), the complement to the Dollars brand. BulkUnmatchState typed with
null-safe setState callbacks. typecheck 0, lint 0 errors, build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:03:09 -05:00
|
|
|
}: PaymentHistoryListProps) {
|
2026-07-03 19:11:05 -05:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div className="mb-3 flex items-center justify-between gap-3">
|
|
|
|
|
<div>
|
2026-07-06 14:23:53 -05:00
|
|
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
|
|
|
Payment history
|
|
|
|
|
</p>
|
2026-07-03 19:11:05 -05:00
|
|
|
<p className="text-[11px] text-muted-foreground/75">{payments.length} recorded</p>
|
|
|
|
|
</div>
|
2026-07-06 14:23:53 -05:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
disabled={paymentBusy}
|
|
|
|
|
onClick={onAdd}
|
|
|
|
|
className="h-8 gap-2 text-xs"
|
|
|
|
|
>
|
2026-07-03 19:11:05 -05:00
|
|
|
<Plus className="h-3.5 w-3.5" />
|
|
|
|
|
Add Payment
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{paymentsLoading ? (
|
|
|
|
|
<div className="rounded-lg border border-border/60 bg-muted/20 px-3 py-8 text-center text-sm text-muted-foreground">
|
|
|
|
|
Loading payment history...
|
|
|
|
|
</div>
|
|
|
|
|
) : payments.length === 0 ? (
|
|
|
|
|
<div className="flex flex-col items-center gap-1.5 rounded-lg bg-muted/20 px-3 py-8 text-center">
|
|
|
|
|
<p className="text-sm font-medium text-muted-foreground">No payments yet</p>
|
2026-07-06 14:23:53 -05:00
|
|
|
<p className="text-xs text-muted-foreground/60">
|
|
|
|
|
Use the form below to record the first payment.
|
|
|
|
|
</p>
|
2026-07-03 19:11:05 -05:00
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="max-h-52 space-y-2 overflow-y-auto pr-1">
|
2026-07-06 14:23:53 -05:00
|
|
|
{payments.map((payment) => {
|
2026-07-03 19:11:05 -05:00
|
|
|
const linkedPayment = isTransactionLinkedPayment(payment);
|
|
|
|
|
const historyOnly = isHistoryOnlyPayment(payment);
|
|
|
|
|
return (
|
2026-07-06 14:23:53 -05:00
|
|
|
<div
|
|
|
|
|
key={payment.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-start justify-between gap-3 rounded-lg border px-3 py-2.5',
|
|
|
|
|
historyOnly
|
2026-07-10 18:03:15 -05:00
|
|
|
? 'border-amber-500/25 bg-amber-500/6 opacity-85'
|
2026-07-06 14:23:53 -05:00
|
|
|
: 'border-border/60 bg-background/35',
|
|
|
|
|
)}
|
|
|
|
|
>
|
2026-07-03 19:11:05 -05:00
|
|
|
<div className="min-w-0">
|
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
2026-07-06 14:23:53 -05:00
|
|
|
<p
|
|
|
|
|
className={cn(
|
|
|
|
|
'font-mono text-sm font-semibold',
|
|
|
|
|
historyOnly
|
|
|
|
|
? 'text-muted-foreground line-through decoration-amber-500/70'
|
|
|
|
|
: 'text-foreground',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{fmt(payment.amount)}
|
|
|
|
|
</p>
|
|
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
'rounded-md border px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
|
|
|
|
|
paymentSourceTone(payment.payment_source),
|
|
|
|
|
)}
|
|
|
|
|
>
|
2026-07-03 19:11:05 -05:00
|
|
|
{paymentSourceLabel(payment.payment_source)}
|
|
|
|
|
</span>
|
|
|
|
|
{historyOnly && (
|
|
|
|
|
<span className="rounded-md border border-amber-500/30 bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-600 dark:text-amber-300">
|
|
|
|
|
History only
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
2026-07-06 14:23:53 -05:00
|
|
|
{fmtDate(payment.paid_date)} ·{' '}
|
|
|
|
|
{payment.method ||
|
|
|
|
|
(payment.payment_source === 'transaction_match' ? 'Synced' : 'manual')}
|
2026-07-03 19:11:05 -05:00
|
|
|
</p>
|
|
|
|
|
{payment.notes && (
|
2026-07-06 14:23:53 -05:00
|
|
|
<p className="mt-0.5 truncate text-xs text-muted-foreground/80">
|
|
|
|
|
{payment.notes}
|
|
|
|
|
</p>
|
2026-07-03 19:11:05 -05:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex shrink-0 gap-1">
|
|
|
|
|
{historyOnly ? (
|
|
|
|
|
<span className="inline-flex h-8 items-center rounded-md border border-amber-500/25 bg-amber-500/10 px-2 text-[11px] font-medium text-amber-600 dark:text-amber-300">
|
|
|
|
|
Overridden
|
|
|
|
|
</span>
|
|
|
|
|
) : linkedPayment ? (
|
|
|
|
|
<span className="inline-flex h-8 items-center gap-1.5 rounded-md border border-primary/20 bg-primary/5 px-2 text-[11px] font-medium text-primary">
|
|
|
|
|
<Link2 className="h-3.5 w-3.5" />
|
|
|
|
|
Matched
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
2026-07-06 14:23:53 -05:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="icon"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
disabled={paymentBusy}
|
|
|
|
|
onClick={() => onEdit(payment)}
|
|
|
|
|
className="h-8 w-8"
|
|
|
|
|
aria-label="Edit payment"
|
|
|
|
|
>
|
2026-07-03 19:11:05 -05:00
|
|
|
<Pencil className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
2026-07-06 14:23:53 -05:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="icon"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
disabled={paymentBusy}
|
|
|
|
|
onClick={() => onDelete(payment)}
|
|
|
|
|
className="h-8 w-8 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
|
|
|
aria-label="Remove payment"
|
|
|
|
|
>
|
2026-07-03 19:11:05 -05:00
|
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|