Files
enkelbudget/client/src/components/LoanCard.jsx
T
michaelswanson 860d5f55cc Initial commit
2026-06-25 19:58:40 +00:00

166 lines
7.2 KiB
React
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { Pencil, Trash2, ChevronDown, ChevronUp, Plus, CreditCard } from 'lucide-react';
import { fmt, estimatedPayoff, today, parseAmount } from '../utils.js';
import Modal from './Modal.jsx';
function PaymentForm({ loan, onSave, onClose }) {
const [amount, setAmount] = useState(loan.monthly_payment ?? '');
const [date, setDate] = useState(today());
const [notes, setNotes] = useState('');
function submit(e) {
e.preventDefault();
if (isNaN(parseAmount(amount))) return;
onSave({ amount: parseAmount(amount), payment_date: date, notes: notes.trim() || null });
onClose();
}
return (
<form onSubmit={submit} className="space-y-3">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Belopp (kr)</label>
<input
type="text" inputMode="decimal"
className="w-full border border-slate-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={amount} onChange={e => setAmount(e.target.value)} required autoFocus
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Datum</label>
<input
type="date"
className="w-full border border-slate-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={date} onChange={e => setDate(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Anteckning (valfri)</label>
<input
className="w-full border border-slate-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={notes} onChange={e => setNotes(e.target.value)} placeholder="Valfri notering"
/>
</div>
<div className="flex gap-2 pt-1">
<button type="button" onClick={onClose} className="flex-1 py-2.5 rounded-xl border border-slate-200 text-sm font-medium text-slate-600 hover:bg-slate-50">
Avbryt
</button>
<button type="submit" className="flex-1 py-2.5 rounded-xl bg-green-600 text-white text-sm font-medium hover:bg-green-700">
Registrera
</button>
</div>
</form>
);
}
export default function LoanCard({ loan, payments = [], onPayment, onEdit, onDelete }) {
const [expanded, setExpanded] = useState(false);
const [payModal, setPayModal] = useState(false);
const progress = loan.original_amount > 0
? Math.min(100, Math.round(((loan.original_amount - loan.current_balance) / loan.original_amount) * 100))
: 0;
const payoff = estimatedPayoff(loan.current_balance, loan.monthly_payment, loan.interest_rate);
const isPaidOff = loan.current_balance <= 0;
return (
<div className={`bg-white rounded-2xl shadow-sm border overflow-hidden mb-3 ${!loan.is_active ? 'opacity-60' : 'border-slate-200'}`}>
<div className="px-4 py-4">
<div className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0 flex items-center gap-2">
<CreditCard size={16} className="text-blue-500 shrink-0 mt-0.5" />
<div className="min-w-0">
<h3 className="truncate font-semibold text-slate-800 text-sm">{loan.name}</h3>
{loan.notes && <p className="text-xs text-slate-400 mt-0.5">{loan.notes}</p>}
</div>
</div>
<div className="flex flex-wrap gap-1 shrink-0">
{loan.is_active && (
<button
onClick={() => setPayModal(true)}
className="flex items-center gap-1 text-xs bg-green-50 text-green-600 px-2.5 py-1 rounded-lg font-medium hover:bg-green-100"
>
<Plus size={12} /> Betala
</button>
)}
<button onClick={onEdit} className="p-1.5 rounded-lg hover:bg-slate-100 text-slate-400">
<Pencil size={14} />
</button>
<button onClick={onDelete} className="p-1.5 rounded-lg hover:bg-red-50 text-slate-400 hover:text-red-500">
<Trash2 size={14} />
</button>
</div>
</div>
<div className="mb-2.5">
<div className="flex justify-between text-xs text-slate-500 mb-1.5">
<span>{isPaidOff ? 'Betalt!' : `Saldo: ${fmt(loan.current_balance)}`}</span>
<span className="font-medium text-slate-700">{progress}% avbetalt</span>
</div>
<div className="h-2.5 bg-slate-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-500"
style={{
width: `${progress}%`,
backgroundColor: isPaidOff ? '#16A34A' : progress > 75 ? '#22C55E' : progress > 40 ? '#3B82F6' : '#6366F1',
}}
/>
</div>
</div>
<div className="grid grid-cols-1 gap-2 text-center sm:grid-cols-3">
<div className="bg-slate-50 rounded-xl px-2 py-1.5">
<p className="text-xs text-slate-400">Ursprung</p>
<p className="text-xs font-semibold text-slate-700 mt-0.5">{fmt(loan.original_amount)}</p>
</div>
<div className="bg-slate-50 rounded-xl px-2 py-1.5">
<p className="text-xs text-slate-400">Månadsbet.</p>
<p className="text-xs font-semibold text-slate-700 mt-0.5">{fmt(loan.monthly_payment)}</p>
</div>
<div className="bg-slate-50 rounded-xl px-2 py-1.5">
<p className="text-xs text-slate-400">Klar ca</p>
<p className="text-xs font-semibold text-slate-700 mt-0.5">{payoff ?? ''}</p>
</div>
</div>
{loan.interest_rate > 0 && (
<p className="text-xs text-slate-400 mt-2">Ränta: {loan.interest_rate}%</p>
)}
</div>
{payments.length > 0 && (
<>
<button
onClick={() => setExpanded(e => !e)}
className="w-full flex items-center justify-between px-4 py-2 border-t border-slate-100 text-xs text-slate-500 hover:bg-slate-50 transition-colors"
>
<span>Betalningshistorik ({payments.length})</span>
{expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
{expanded && (
<div className="border-t border-slate-100 divide-y divide-slate-50 max-h-48 overflow-y-auto">
{payments.map(p => (
<div key={p.id} className="flex items-center justify-between px-4 py-2">
<span className="text-xs text-slate-500">{p.payment_date}</span>
{p.notes && <span className="text-xs text-slate-400 flex-1 mx-2 truncate">{p.notes}</span>}
<span className="text-xs font-semibold text-green-600">{fmt(p.amount)}</span>
</div>
))}
</div>
)}
</>
)}
{payModal && (
<Modal title={`Registrera betalning ${loan.name}`} onClose={() => setPayModal(false)}>
<PaymentForm
loan={loan}
onSave={(data) => { onPayment(loan.id, data); setPayModal(false); }}
onClose={() => setPayModal(false)}
/>
</Modal>
)}
</div>
);
}