// 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 &&
}
);
})}
{/* 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('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 => (
| {h} |
))}
{filtered.map((c, i) => (
setSelected(c)}
style={{ borderBottom: ie.currentTarget.style.background='var(--bg-hover)'}
onMouseLeave={e=>e.currentTarget.style.background='transparent'}
>
|
|
{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])} />}
);
};
})();