// Payments.jsx — Płatności i raty
(function () {
const { useState, useEffect } = React;
const statusConfig = {
paid: { label:'Opłacona', color:'#22C55E' },
pending: { label:'Oczekująca', color:'#4DA3FF' },
overdue: { label:'Zaległa', color:'#EF4444' },
};
function fmtPLN(v) {
return Math.round(v).toLocaleString('pl-PL') + ' zł';
}
function fmtVal(v) {
if (v >= 1000000) return (v/1000000).toFixed(1).replace('.',',') + ' M zł';
if (v >= 1000) return Math.round(v/1000) + ' k zł';
return Math.round(v) + ' zł';
}
function fmtDue(due) {
const [y,m,d] = due.split('-');
const months = ['sty','lut','mar','kwi','maj','cze','lip','sie','wrz','paź','lis','gru'];
return `${parseInt(d)} ${months[parseInt(m)-1]} ${y}`;
}
function planProgress(plan) {
const paid = plan.installments.filter(i=>i.status==='paid').reduce((s,i)=>s+i.amount,0);
const pct = plan.total > 0 ? Math.round((paid/plan.total)*100) : 0;
return { paid, pct };
}
function planStatus(plan) {
if (plan.installments.some(i=>i.status==='overdue')) return { label:'Zaległość', color:'#EF4444' };
if (plan.installments.every(i=>i.status==='paid')) return { label:'Zakończony', color:'#22C55E' };
return { label:'W trakcie', color:'#4DA3FF' };
}
const inputStyle = { width:'100%', padding:'9px 12px', borderRadius:9, border:'1px solid var(--bdr)', background:'var(--bg-input)', color:'var(--t1)', fontSize:13, outline:'none' };
const labelStyle = { fontSize:11, fontWeight:600, color:'var(--t3)', marginBottom:5, display:'block', textTransform:'uppercase', letterSpacing:0.5 };
function StatusBadge({ status }) {
const s = statusConfig[status] || statusConfig.pending;
return (
{s.label}
);
}
function ProgressBar({ pct, color }) {
return (
);
}
// ── New payment plan modal ─────────────────────────────────────────
function NewPlanModal({ onClose, onSave, prefill }) {
const clients = window.SEED.clients || [];
const [clientId, setClientId] = useState(prefill?.clientId || clients[0]?.id || '');
const [total, setTotal] = useState(prefill?.total || '');
const [count, setCount] = useState('3');
const [firstDue, setFirstDue] = useState('2026-08-01');
const [interval, setInterval] = useState('monthly');
const client = clients.find(c => c.id === clientId);
const canSave = !!client && Number(total) > 0;
const buildSchedule = () => {
const n = interval === 'once' ? 1 : Number(count) || 1;
const totalNum = Number(total) || 0;
const base = Math.floor(totalNum / n);
const remainder = totalNum - base * n;
const stepDays = interval === 'biweekly' ? 14 : 30;
const start = new Date(firstDue + 'T00:00:00');
const rows = [];
for (let i = 0; i < n; i++) {
const d = new Date(start.getTime() + i * stepDays * 86400000);
const dueStr = d.toISOString().slice(0,10);
rows.push({
id: 'r' + (i+1),
label: n === 1 ? 'Płatność jednorazowa' : `Rata ${i+1}/${n}` + (i===0 ? ' — zaliczka' : i===n-1 ? ' — końcowa' : ''),
amount: i === n-1 ? base + remainder : base,
due: dueStr, status: 'pending', paidDate: null,
});
}
return rows;
};
const submit = () => {
if (!canSave) return;
onSave({
id: 'PL' + Math.random().toString(36).slice(2,7).toUpperCase(),
clientId: client.id, company: client.company, total: Number(total),
installments: buildSchedule(),
});
onClose();
};
return (
<>
Nowy plan płatności
Harmonogram rat wygeneruje się automatycznie
({value:c.id,label:c.company}))} />
setTotal(e.target.value)} placeholder="0" />
{interval !== 'once' && (
({value:v,label:v+' raty'}))} />
)}
setFirstDue(e.target.value)} />
>
);
}
// ── Plan drawer — schedule + mark as paid ──────────────────────────
function PlanDrawer({ plan, onClose, onMarkPaid }) {
useEffect(() => {
const esc = (e) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', esc);
return () => document.removeEventListener('keydown', esc);
}, []);
const { paid, pct } = planProgress(plan);
const st = planStatus(plan);
return (
<>
{plan.company}
{plan.id} · {fmtPLN(plan.total)}
{st.label}
Opłacono {fmtPLN(paid)} z {fmtPLN(plan.total)}
{pct}%
Harmonogram rat
{plan.installments.map(inst => (
{inst.label}
{inst.status === 'paid' ? `Zapłacono: ${fmtDue(inst.paidDate)}` : `Termin: ${fmtDue(inst.due)}`}
{inst.status !== 'paid' && (
)}
))}
>
);
}
// ── Payments (main export) ────────────────────────────────────────
window.Payments = function Payments({ prefillPlan, onPrefillConsumed }) {
const [plans, setPlans] = useState(() => window.SEED.payments || []);
const [search, setSearch] = useState('');
const [selected, setSelected] = useState(null);
const [showNew, setShowNew] = useState(false);
const [pending, setPending] = useState(null);
useEffect(() => {
if (prefillPlan) {
setPending(prefillPlan);
setShowNew(true);
onPrefillConsumed && onPrefillConsumed();
}
}, [prefillPlan]);
const markPaid = (planId, instId) => {
const today = new Date();
const pad = n => String(n).padStart(2,'0');
const paidDateStr = `${today.getFullYear()}-${pad(today.getMonth()+1)}-${pad(today.getDate())}`;
setPlans(prev => prev.map(p => p.id !== planId ? p : {
...p, installments: p.installments.map(i => i.id === instId ? { ...i, status:'paid', paidDate: paidDateStr } : i),
}));
setSelected(prev => {
if (!prev || prev.id !== planId) return prev;
return { ...prev, installments: prev.installments.map(i => i.id === instId ? { ...i, status:'paid', paidDate: paidDateStr } : i) };
});
};
const filtered = plans.filter(p => p.company.toLowerCase().includes(search.toLowerCase()));
const totalDue = plans.reduce((s,p) => s + p.installments.filter(i=>i.status!=='paid').reduce((a,i)=>a+i.amount,0), 0);
const totalPaid = plans.reduce((s,p) => s + p.installments.filter(i=>i.status==='paid').reduce((a,i)=>a+i.amount,0), 0);
const overdueInsts = plans.flatMap(p=>p.installments).filter(i=>i.status==='overdue');
const overdueSum = overdueInsts.reduce((s,i)=>s+i.amount,0);
const activePlans = plans.filter(p => p.installments.some(i=>i.status!=='paid')).length;
return (
{[
{ label:'Do zapłaty (aktywne raty)', val: fmtVal(totalDue), color:'#4DA3FF' },
{ label:'Opłacone łącznie', val: fmtVal(totalPaid), color:'#22C55E' },
{ label:'Zaległe płatności', val: `${overdueInsts.length} · ${fmtVal(overdueSum)}`, color:'#EF4444' },
{ label:'Aktywne plany płatności', val: String(activePlans), color:'#7C5CFF' },
].map((s,i) => (
))}
setSearch(e.target.value)} placeholder="Szukaj klientów…"
style={{ width:'100%', padding:'9px 12px 9px 34px', borderRadius:10, border:'1px solid var(--bdr)', background:'var(--bg-input)', color:'var(--t1)', fontSize:13, outline:'none' }} />
{filtered.map(plan => {
const { paid, pct } = planProgress(plan);
const st = planStatus(plan);
const next = plan.installments.find(i => i.status !== 'paid');
return (
setSelected(plan)} className="glass-card" style={{ padding:'16px 20px', cursor:'pointer', display:'grid', gridTemplateColumns:'1.6fr 1fr 1fr 0.9fr', gap:16, alignItems:'center' }}>
{plan.company}
{plan.id} · {plan.installments.length} rat
{fmtPLN(paid)} / {fmtPLN(plan.total)}
{pct}%
{next ? (
<>
{next.status==='overdue' && }{fmtPLN(next.amount)}
Termin: {fmtDue(next.due)}
>
) : (
Rozliczone
)}
{st.label}
);
})}
{filtered.length === 0 && (
Brak planów płatności dla podanego filtra
)}
{selected &&
p.id===selected.id) || selected} onClose={()=>setSelected(null)} onMarkPaid={markPaid} />}
{showNew && { setShowNew(false); setPending(null); }}
onSave={p => setPlans(prev => [p, ...prev])}
prefill={pending}
/>}
);
};
})();