// Kanban.jsx — Drag & Drop Sales Funnel
(function () {
const { useState, useRef } = React;
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 sourceColors = {
'Google': '#7C5CFF',
'Polecenia': '#4DA3FF',
'Social': '#00D9FF',
'Formularz': '#22C55E',
};
const sourceLabels = { 'Google':'Google Ads', 'Polecenia':'Polecenia', 'Social':'Meta Ads', 'Formularz':'Formularz' };
// ── Lead Detail Drawer ───────────────────────────────────────────
function LeadDrawer({ lead, columns, onClose, onMove, onAddNote }) {
const [noteText, setNoteText] = React.useState('');
if (!lead) return null;
const col = columns.find(c => c.id === lead.column);
return (
<>
{lead.initials}
{lead.company}
{lead.person}
{col?.label}
{sourceLabels[lead.source] || lead.source}
Szczegóły leada
{[
{ icon:'◈', label:`Wartość: ${fmtVal(lead.value)}` },
{ icon:'✉', label:`Osoba kontaktowa: ${lead.person}` },
{ icon:'●', label:`Źródło: ${sourceLabels[lead.source] || lead.source}` },
].map((r,i) => (
{r.icon}
{r.label}
))}
Przenieś do etapu
{columns.map(c => (
))}
Notatki
{lead.notes &&
{lead.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(lead.id, noteText.trim()); setNoteText(''); } }}
/>
>
);
}
// ── Kanban Card ───────────────────────────────────────────────────
function KanbanCard({ lead, onDragStart, onOpen }) {
const [hov, setHov] = useState(false);
const dragStarted = useRef(false);
return (
{ dragStarted.current = true; onDragStart(lead.id); }}
onClick={() => { if (!dragStarted.current) onOpen(lead); dragStarted.current = false; }}
onMouseEnter={() => setHov(true)}
onMouseLeave={() => setHov(false)}
style={{
background: hov ? 'var(--bg2)' : 'var(--bg1)',
border: '1px solid var(--bdr)',
borderRadius: 12, padding: '12px 14px', cursor: 'pointer',
transition: 'all .18s ease',
transform: hov ? 'translateY(-2px)' : 'none',
boxShadow: hov ? '0 8px 24px rgba(0,0,0,0.3)' : '0 2px 8px rgba(0,0,0,0.15)',
userSelect: 'none',
}}
>
{/* Header: avatar + company */}
{lead.initials}
{lead.company}
{lead.person}
{/* Value + source */}
{fmtVal(lead.value)}
{sourceLabels[lead.source] || lead.source}
);
}
// ── Kanban Column ─────────────────────────────────────────────────
function KanbanColumn({ col, leads, onDragStart, onDrop, onOpen }) {
const [dragOver, setDragOver] = useState(false);
const total = leads.reduce((s, l) => s + l.value, 0);
return (
{ e.preventDefault(); setDragOver(true); }}
onDragLeave={() => setDragOver(false)}
onDrop={() => { setDragOver(false); onDrop(col.id); }}
style={{
minWidth: 220, maxWidth: 240, flex: '1 1 220px',
display: 'flex', flexDirection: 'column', gap: 0,
background: dragOver ? 'rgba(124,92,255,0.06)' : 'transparent',
border: dragOver ? '1.5px dashed rgba(124,92,255,0.4)' : '1.5px dashed transparent',
borderRadius: 16, transition: 'all .18s ease',
}}
>
{/* Column header */}
{col.label}
{leads.length}
{leads.length > 0 && (
{fmtVal(total)}
)}
{/* Cards */}
{leads.map(lead => (
))}
{leads.length === 0 && (
Upuść tutaj
)}
);
}
// ── Kanban (main export) ──────────────────────────────────────────
window.Kanban = function Kanban({ leads, setLeads }) {
const dragId = useRef(null);
const S = window.SEED;
const [openLead, setOpenLead] = useState(null);
const handleDragStart = (id) => { dragId.current = id; };
const handleDrop = (colId) => {
if (!dragId.current) return;
setLeads(prev => prev.map(l => l.id === dragId.current ? { ...l, column: colId } : l));
dragId.current = null;
};
const moveLead = (id, colId) => {
setLeads(prev => prev.map(l => l.id === id ? { ...l, column: colId } : l));
setOpenLead(prev => prev && prev.id === id ? { ...prev, column: colId } : prev);
};
const addLeadNote = (id, text) => {
setLeads(prev => prev.map(l => l.id === id ? { ...l, notes: l.notes ? l.notes + '\n' + text : text } : l));
setOpenLead(prev => prev && prev.id === id ? { ...prev, notes: prev.notes ? prev.notes + '\n' + text : text } : prev);
};
const totalWon = leads.filter(l => l.column === 'won').reduce((s, l) => s + l.value, 0);
const totalPipeline = leads.filter(l => !['won','lost'].includes(l.column)).reduce((s, l) => s + l.value, 0);
return (
{/* Stats bar */}
{[
{ label: 'W toku', val: fmtVal(totalPipeline), color: '#7C5CFF' },
{ label: 'Wygranych', val: fmtVal(totalWon), color: '#22C55E' },
{ label: 'Łącznie leadów', val: leads.length, color: '#4DA3FF' },
].map(s => (
))}
{/* Board */}
{S.kanbanColumns.map(col => (
l.column === col.id)}
onDragStart={handleDragStart}
onDrop={handleDrop}
onOpen={setOpenLead}
/>
))}
{openLead &&
setOpenLead(null)} onMove={moveLead} onAddNote={addLeadNote} />}
);
};
})();