// Clients.jsx — Table + Side Drawer (function () { const { useState, useEffect } = React; const statusStyle = { 'Aktywny': { bg: 'rgba(34,197,94,.15)', color: '#22C55E', dot: '#22C55E' }, 'W toku': { bg: 'rgba(245,158,11,.15)', color: '#F59E0B', dot: '#F59E0B' }, 'Nieaktywny': { bg: 'rgba(100,116,139,.15)', color: '#94A3B8', dot: '#94A3B8' }, }; const timelineIcon = { deal: { bg: '#22C55E', sym: '✓' }, call: { bg: '#4DA3FF', sym: '☎' }, email: { bg: '#7C5CFF', sym: '✉' }, meeting: { bg: '#F59E0B', sym: '⊙' }, }; 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 v + ' zł'; } const sourceLabel = s => s === 'Social' ? 'Meta Ads' : s === 'Google' ? 'Google Ads' : s; // ── Status pill ─────────────────────────────────────────────────── function StatusPill({ status }) { const s = statusStyle[status] || statusStyle['W toku']; return ( {status} ); } // ── Client Drawer ───────────────────────────────────────────────── function ClientDrawer({ client, onClose, onAddNote }) { const [noteText, setNoteText] = useState(''); useEffect(() => { const esc = (e) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', esc); return () => document.removeEventListener('keydown', esc); }, []); return ( <> {/* Backdrop */}
{/* Drawer */}
{/* Drawer header */}
{client.initials}
{client.company}
{client.person}
{/* Status + source */}
{sourceLabel(client.source)}
{/* Contact info */}
Dane kontaktowe
{[ { icon:'✉', label: client.email }, { icon:'☎', label: client.phone }, { icon:'⊙', label: `Ostatni kontakt: ${client.lastContact}` }, { icon:'◈', label: `${client.deals} ${client.deals === 1 ? 'oferta' : client.deals < 5 ? 'oferty' : 'ofert'} · ${fmtVal(client.value)}` }, ].map((r,i) => (
{r.icon} {r.label}
))}
{/* Notes */}
Notatki
{client.notes &&

{client.notes}

}
setNoteText(e.target.value)} placeholder="Dopisz notatkę…" style={{ flex:1, padding:'8px 10px', borderRadius:8, border:'1px solid var(--bdr)', background:'var(--bg-input)', color:'var(--t1)', fontSize:12.5, outline:'none' }} onKeyDown={e => { if (e.key==='Enter' && noteText.trim()) { onAddNote(client.id, noteText.trim()); setNoteText(''); } }} />
{/* Timeline */}
Historia kontaktów
{(client.timeline || []).map((ev, i) => { const ti = timelineIcon[ev.type] || timelineIcon.meeting; return (
{ti.sym}
{i < client.timeline.length-1 &&
}
{ev.action}
{ev.date}
); })}
{/* Actions */}
); } // ── Add Client Modal ────────────────────────────────────────────── function AddClientModal({ onClose, onSave }) { const [form, setForm] = useState({ company:'', person:'', email:'', phone:'', status:'W toku', source:'Google', value:'' }); const set = (k,v) => setForm(f => ({ ...f, [k]:v })); const canSave = form.company.trim().length > 0 && form.person.trim().length > 0; const submit = () => { if (!canSave) return; const initials = form.company.trim().slice(0,2).toUpperCase(); const colors = ['#7C5CFF','#4DA3FF','#00D9FF','#22C55E','#F59E0B']; const today = new Date(); const dateStr = `${String(today.getDate()).padStart(2,'0')}.${String(today.getMonth()+1).padStart(2,'0')}.${today.getFullYear()}`; onSave({ id:'C'+Math.random().toString(36).slice(2,8).toUpperCase(), company:form.company.trim(), person:form.person.trim(), email:form.email.trim() || '—', phone:form.phone.trim() || '—', status:form.status, source:form.source, value:Number(form.value)||0, lastContact:dateStr, deals:0, initials, avatarColor: colors[Math.floor(Math.random()*colors.length)], timeline:[{ date:dateStr, action:'Klient dodany do systemu', type:'meeting' }], notes:'', }); onClose(); }; 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 }; return ( <>
Nowy klient
set('company',e.target.value)} placeholder="np. Techlab Solutions Sp. z o.o." autoFocus />
set('person',e.target.value)} placeholder="np. Jan Kowalski" />
set('email',e.target.value)} placeholder="jan@firma.pl" />
set('phone',e.target.value)} placeholder="+48 500 000 000" />
set('status',v)} options={['Aktywny','W toku','Nieaktywny']} />
set('source',v)} options={[{value:'Google',label:'Google Ads'},{value:'Polecenia',label:'Polecenia'},{value:'Social',label:'Meta Ads'},{value:'Formularz',label:'Formularz'}]} />
set('value',e.target.value)} placeholder="0" />
); } // ── CSV Import ──────────────────────────────────────────────────── function parseCSV(text) { const lines = text.split(/\r?\n/).filter(l => l.trim().length); if (lines.length < 2) return []; const sep = lines[0].includes(';') ? ';' : ','; const headers = lines[0].split(sep).map(h => h.trim().toLowerCase()); const idx = (names) => headers.findIndex(h => names.some(n => h.includes(n))); const iCompany = idx(['firma','company','nazwa']); const iPerson = idx(['osoba','kontakt','person','imię']); const iEmail = idx(['email','e-mail']); const iPhone = idx(['telefon','phone']); const iValue = idx(['wartość','wartosc','value']); const colors = ['#7C5CFF','#4DA3FF','#00D9FF','#22C55E','#F59E0B']; return lines.slice(1).map(line => { const cells = line.split(sep).map(c => c.trim()); const company = iCompany>=0 ? cells[iCompany] : cells[0] || 'Bez nazwy'; const person = iPerson>=0 ? cells[iPerson] : cells[1] || '—'; return { id:'C'+Math.random().toString(36).slice(2,8).toUpperCase(), company, person, email: iEmail>=0 ? cells[iEmail] : '—', phone: iPhone>=0 ? cells[iPhone] : '—', status:'W toku', source:'Formularz', value: iValue>=0 ? Number(cells[iValue].replace(/\D/g,''))||0 : 0, lastContact:'—', deals:0, initials: company.slice(0,2).toUpperCase(), avatarColor: colors[Math.floor(Math.random()*colors.length)], timeline:[{ date:'importowano', action:'Zaimportowany z pliku CSV/Excel', type:'meeting' }], notes:'', }; }); } // ── Clients (main export) ───────────────────────────────────────── window.Clients = function Clients() { const [clients, setClients] = useState(window.SEED.clients); const [search, setSearch] = useState(''); const [filterStatus, setFilterStatus] = useState('Wszyscy'); const [selected, setSelected] = useState(null); const [showAdd, setShowAdd] = useState(false); const [importMsg, setImportMsg] = useState(''); const fileRef = React.useRef(null); const handleImport = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { const rows = parseCSV(String(ev.target.result)); if (rows.length === 0) { setImportMsg('Nie znaleziono danych w pliku'); } else { setClients(prev => [...rows, ...prev]); setImportMsg(`Zaimportowano ${rows.length} klientów`); } setTimeout(() => setImportMsg(''), 4000); }; reader.readAsText(file); e.target.value = ''; }; const statuses = ['Wszyscy', 'Aktywny', 'W toku', 'Nieaktywny']; const filtered = clients.filter(c => { const matchSearch = c.company.toLowerCase().includes(search.toLowerCase()) || c.person.toLowerCase().includes(search.toLowerCase()); const matchStatus = filterStatus === 'Wszyscy' || c.status === filterStatus; return matchSearch && matchStatus; }); return (
{/* Toolbar */}
🔍 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' }} onFocus={e=>e.target.style.borderColor='rgba(124,92,255,.5)'} onBlur={e=>e.target.style.borderColor='var(--bdr)'} />
{statuses.map(s => ( ))}
{importMsg &&
✓ {importMsg}
} {/* Table */}
{['Firma','Kontakt','Status','Źródło','Wartość','Ost. kontakt'].map(h => ( ))} {filtered.map((c, i) => ( setSelected(c)} style={{ borderBottom: ie.currentTarget.style.background='var(--bg-hover)'} onMouseLeave={e=>e.currentTarget.style.background='transparent'} > ))}
{h}
{c.initials}
{c.company}
{c.person} {sourceLabel(c.source)} {fmtVal(c.value)} {c.lastContact}
{filtered.length === 0 && (
Brak wyników dla podanych filtrów
)}
{selected && setSelected(null)} onAddNote={(id, text) => { setClients(prev => prev.map(c => c.id === id ? { ...c, notes: c.notes ? c.notes + '\n' + text : text } : c)); setSelected(prev => prev && prev.id === id ? { ...prev, notes: prev.notes ? prev.notes + '\n' + text : text } : prev); }} />} {showAdd && setShowAdd(false)} onSave={c => setClients(prev => [c, ...prev])} />}
); }; })();