Initial commit
This commit is contained in:
Executable
+165
@@ -0,0 +1,165 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user