// 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)}`}
{fmtPLN(inst.amount)}
{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) => (
{s.label}
{s.val}
))}
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} />}
); }; })();