// Sales pipeline screen — deal kanban.
(() => {
const { Card, Badge, Avatar } = window.TechyFuelOSDesignSystem_be0222;
const STAGE_CONFIG = [
{ id: 'lead', label: 'New lead', dot: 'var(--slate-400)' },
{ id: 'qualified', label: 'Qualified', dot: 'var(--blue-500)' },
{ id: 'proposal', label: 'Proposal sent', dot: 'var(--violet-500)' },
{ id: 'negotiation', label: 'Negotiation', dot: 'var(--amber-500)' },
{ id: 'won', label: 'Won', dot: 'var(--green-500)' },
{ id: 'lost', label: 'Lost', dot: 'var(--red-400)' },
];
function fmtVal(n) {
n = Number(n) || 0;
if (!n) return '$0';
if (n >= 1000) return '$' + (n / 1000).toFixed(1) + 'K';
return '$' + n;
}
function Deal({ d }) {
const [h, sh] = React.useState(false);
const clientName = d.clients ? d.clients.name : '—';
const assignedName = d.team_members ? d.team_members.name : null;
return (
sh(true)} onMouseLeave={() => sh(false)} style={{ background: 'var(--slate-0)', border: `1px solid ${h ? 'var(--slate-200)' : 'var(--border-subtle)'}`, borderRadius: 'var(--radius-lg)', padding: 11, boxShadow: h ? 'var(--shadow-md)' : 'var(--shadow-xs)', cursor: 'grab', transition: 'all var(--dur-fast) var(--ease-out)' }}>
{clientName}
{d.probability >= 80 && }
{d.title}
{fmtVal(d.value)}
{d.probability != null &&
{d.probability}% prob.}
{assignedName &&
}
);
}
function Pipeline() {
useLucide();
const [stageMap, setStageMap] = React.useState(() => {
const m = {};
STAGE_CONFIG.forEach(s => { m[s.id] = []; });
return m;
});
const [totals, setTotals] = React.useState({ count: 0, value: 0 });
const [clients, setClients] = React.useState([]);
const [modalOpen, setModalOpen] = React.useState(false);
const [saving, setSaving] = React.useState(false);
const [form, setForm] = React.useState({ title: '', client_id: '', value: '', stage: 'lead', probability: '' });
function set(k, v) { setForm(f => ({ ...f, [k]: v })); }
React.useEffect(() => {
if (!window.API) return;
(async () => {
try {
const r = await window.API.getPipeline();
if (r.data) {
const m = {};
STAGE_CONFIG.forEach(s => { m[s.id] = []; });
r.data.forEach(d => {
const key = d.stage || 'lead';
if (!m[key]) m[key] = [];
m[key].push(d);
});
setStageMap(m);
const openDeals = r.data.filter(d => d.stage !== 'won' && d.stage !== 'lost');
setTotals({ count: openDeals.length, value: openDeals.reduce((s, d) => s + (Number(d.value) || 0), 0) });
}
} catch {}
try {
const r = await window.API.getClients();
if (r.data) setClients(r.data);
} catch {}
})();
}, []);
async function handleAddDeal() {
if (!form.title.trim()) return;
setSaving(true);
try {
const payload = { title: form.title, stage: form.stage };
if (form.client_id) payload.client_id = form.client_id;
if (form.value) payload.value = Number(form.value);
if (form.probability) payload.probability = Number(form.probability);
if (window.API) {
const { data, error } = await window.API.createDeal(payload);
if (!error && data) {
const clientName = clients.find(c => c.id === form.client_id)?.name || null;
const newDeal = { ...data, clients: clientName ? { name: clientName } : null };
const stage = data.stage || 'lead';
setStageMap(prev => ({ ...prev, [stage]: [...(prev[stage] || []), newDeal] }));
if (stage !== 'won' && stage !== 'lost') {
setTotals(prev => ({ count: prev.count + 1, value: prev.value + (data.value || 0) }));
}
}
}
setModalOpen(false);
setForm({ title: '', client_id: '', value: '', stage: 'lead', probability: '' });
} finally { setSaving(false); }
}
return (
Sales pipeline
{totals.count} open deals · {fmtVal(totals.value)} weighted value
{STAGE_CONFIG.map(s => {
const deals = stageMap[s.id] || [];
const stageTotal = deals.reduce((a, d) => a + (Number(d.value) || 0), 0);
return (
{s.label}
{fmtVal(stageTotal)}
{deals.map((d, i) => )}
);
})}
setModalOpen(false)} title="Add deal" onSubmit={handleAddDeal} loading={saving} submitLabel="Add deal">
set('title', e.target.value)} />
set('value', e.target.value)} />
set('probability', e.target.value)} />
);
}
Object.assign(window, { Pipeline });
})();