// ---- Adapters: DB row <-> UI shape ---- const STATUS_DB_TO_UI = { ativo: 'Ativo', em_avaliacao: 'Em Avaliação', inativo: 'Inativo', bloqueado: 'Bloqueado', }; const STATUS_UI_TO_DB = { 'Ativo': 'ativo', 'Em Avaliação': 'em_avaliacao', 'Inativo': 'inativo', 'Bloqueado': 'bloqueado', }; const toUiSupplier = (row) => ({ ...row, contact: row.contact_name || '—', whats: row.whatsapp || '—', response: row.response_rate ?? 0, avgDelivery: row.avg_delivery || '—', status: STATUS_DB_TO_UI[row.status] || 'Em Avaliação', }); function ScreenSuppliers({ setRoute }) { const { CATEGORIES } = window.SCP; const [q, setQ] = useState(''); const [cat, setCat] = useState('todas'); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [loadErr, setLoadErr] = useState(null); const [showNew, setShowNew] = useState(false); const [highlightId, setHighlightId] = useState(null); const [openSup, setOpenSup] = useState(null); useEffect(() => { let mounted = true; (async () => { try { const data = await window.scpDb.suppliers.list(); if (mounted) setRows(data.map(toUiSupplier)); } catch (e) { if (mounted) setLoadErr(e.message || 'Falha ao carregar fornecedores'); } finally { if (mounted) setLoading(false); } })(); return () => { mounted = false; }; }, []); const list = rows.filter(s => (cat === 'todas' || (s.cats || []).includes(cat)) && (!q || (s.name + (s.cnpj || '') + (s.city || '')).toLowerCase().includes(q.toLowerCase())) ); const statusTone = { Ativo: 'emerald', 'Em Avaliação': 'amber', Inativo: 'muted', Bloqueado: 'coral' }; const kpis = useMemo(() => { const ativos = rows.filter(r => r.status === 'Ativo').length; const avaliacao = rows.filter(r => r.status === 'Em Avaliação').length; const bloqueados = rows.filter(r => r.status === 'Bloqueado').length; const scoreSum = rows.reduce((s, r) => s + (Number(r.score) || 0), 0); const scoreAvg = rows.length ? (scoreSum / rows.length) : 0; const respSum = rows.reduce((s, r) => s + (Number(r.response) || 0), 0); const respAvg = rows.length ? Math.round(respSum / rows.length) : 0; const catCount = {}; rows.forEach(r => (r.cats || []).forEach(k => { catCount[k] = (catCount[k] || 0) + 1; })); let topCat = null, topCount = 0; Object.entries(catCount).forEach(([k, n]) => { if (n > topCount) { topCount = n; topCat = k; } }); const topCatLabel = topCat ? (CATEGORIES.find(c => c.key === topCat)?.label || topCat) : '—'; return { ativos, avaliacao, bloqueados, scoreAvg, respAvg, topCatLabel, topCatCount: topCount }; }, [rows]); const handleCreate = async (data) => { try { const payload = { name: data.name, cnpj: data.cnpj || null, city: data.city || null, address: data.address || null, contact_name: data.contact || null, whatsapp: data.whats || null, email: data.email || null, cats: data.cats?.length ? data.cats : ['secos'], score: 75, wins: 0, response_rate: 0, avg_delivery: null, status: 'em_avaliacao', }; const created = await window.scpDb.suppliers.create(payload); const uiRow = toUiSupplier(created); setRows(prev => [uiRow, ...prev]); setShowNew(false); setHighlightId(uiRow.id); window.scpToast('Fornecedor cadastrado', { kind: 'emerald', sub: uiRow.name }); setTimeout(() => setHighlightId(null), 2400); } catch (e) { window.scpToast('Erro ao cadastrar', { kind: 'coral', sub: e.message || 'Tente novamente' }); } }; const handleDelete = async (supplier) => { if (!confirm(`Desativar "${supplier.name}"? Ele sai da lista mas o histórico (cotações, pedidos) fica preservado.`)) return; try { await window.scpDb.suppliers.remove(supplier.id); setRows(prev => prev.filter(r => r.id !== supplier.id)); setOpenSup(null); window.scpToast('Fornecedor desativado', { kind: 'amber', sub: supplier.name }); } catch (e) { window.scpToast('Erro ao desativar', { kind: 'coral', sub: e.message || 'Tente novamente' }); } }; return (
}/> }/> }/> }/>
{loadErr && ( Falha ao carregar fornecedores:{' '} {loadErr}
Verifique se o schema foi aplicado no Supabase e se o seed-suppliers.sql foi rodado.
)}
{CATEGORIES.map(c => ( ))}
} placeholder="Buscar fornecedor…" value={q} onChange={e => setQ(e.target.value)} /> } onClick={() => setShowNew(true)}>Novo Fornecedor
{loading && (
Carregando fornecedores…
)} {!loading && list.length === 0 && (

{rows.length === 0 ? 'Nenhum fornecedor cadastrado ainda.' : 'Nenhum fornecedor com este filtro.'}

{rows.length === 0 && ( } onClick={() => setShowNew(true)}> Cadastrar primeiro fornecedor )}
)} {!loading && list.map(s => (
setOpenSup(s)} role="button" tabIndex={0} onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpenSup(s); } }} >
{s.name} {s.status}
CNPJ {s.cnpj} {s.city} {s.contact} · {s.whats}
{s.cats.map(k => { const c = CATEGORIES.find(x => x.key === k); return c ? {c.label} : null; })}
Score{s.score}
Vitórias{s.wins}
Resposta{s.response}%
Entrega{s.avgDelivery}
e.stopPropagation()}> } title="WhatsApp"/> } title="E-mail"/> } title="Editar"/>
))}
{showNew && setShowNew(false)} onCreate={handleCreate} categories={CATEGORIES} />} setOpenSup(null)} eyebrow={openSup?.id} title={openSup?.name || ''} headerRight={openSup && {openSup.status}} footer={openSup && ( <> } onClick={() => handleDelete(openSup)} style={{ color: 'oklch(0.82 0.14 25)', marginRight: 'auto' }}>Excluir } onClick={() => { if (openSup.email && openSup.email !== '—') window.open(`mailto:${openSup.email}?subject=${encodeURIComponent('Contato · ' + window.SCP.COMPANY.name)}`, '_self'); else window.scpToast('Sem e-mail cadastrado', { kind: 'amber', sub: openSup.name }); }}>E-mail } onClick={() => { const num = (openSup.whats || '').replace(/\D/g, ''); if (num) window.open(`https://wa.me/55${num}`, '_blank', 'noopener'); else window.scpToast('Sem WhatsApp cadastrado', { kind: 'amber' }); }}>Abrir WhatsApp )} > {openSup && }
); } function SupplierDetail({ supplier: s, categories }) { const { BRL } = window.SCP; const [recentOrders, setRecentOrders] = useState(null); const [products, setProducts] = useState(null); useEffect(() => { let mounted = true; (async () => { const [orders, prods] = await Promise.all([ window.scpDb.suppliers.ordersBy(s.id), window.scpDb.suppliers.productsBy(s.id), ]); if (!mounted) return; setRecentOrders(orders); setProducts(prods); })(); return () => { mounted = false; }; }, [s.id]); return ( <>
{s.name}
{s.city} · {s.id}
{s.cats.map(k => { const c = categories.find(x => x.key === k); return c ? {c.label} : null; })}

Performance

Score {s.score} Pontualidade · preço · vitórias
Vitórias {s.wins} Cotações vencidas (90d)
Taxa de resposta {s.response}% Cotações respondidas no prazo
Prazo médio {s.avgDelivery} Entrega após confirmação

Contato

CNPJ
{s.cnpj}
Cidade
{s.city}
Endereço
{s.address || '—'}
Responsável
{s.contact}
WhatsApp
{s.whats}
E-mail
{s.email || '—'}

Produtos fornecidos

{products === null ? (
Carregando…
) : products.length ? (
{products.map(p => (
{p.name}
{p.code} · {p.unit}
{p.last_price ? BRL(p.last_price) : '—'}
))}
) : (
Nenhum produto vinculado a este fornecedor.
)}

Últimos pedidos

{recentOrders === null ? (
Carregando…
) : recentOrders.length ? (
{recentOrders.map(o => (
{o.code}
{new Date(o.emitted_at).toLocaleDateString('pt-BR')}
{BRL(o.total)}
))}
) : (
Sem pedidos recentes deste fornecedor.
)}
); } function NewSupplierModal({ onClose, onCreate, categories }) { const [name, setName] = useState(''); const [cnpj, setCnpj] = useState(''); const [city, setCity] = useState(''); const [address, setAddress] = useState(''); const [contact, setContact] = useState(''); const [whats, setWhats] = useState(''); const [email, setEmail] = useState(''); const [cats, setCats] = useState([]); const emailValid = !email || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()); const canSubmit = name.trim().length >= 2 && emailValid; const toggleCat = (k) => { setCats(cats.includes(k) ? cats.filter(x => x !== k) : [...cats, k]); }; const submit = () => { if (!canSubmit) return; onCreate({ name: name.trim(), cnpj: cnpj.trim(), city: city.trim(), address: address.trim(), contact: contact.trim(), whats: whats.trim(), email: email.trim(), cats, }); }; return ReactDOM.createPortal((
e.stopPropagation()}>

Novo Fornecedor

} size="sm" onClick={onClose}/>
Razão social / Nome * setName(e.target.value)} autoFocus />
CNPJ setCnpj(e.target.value)} />
Cidade · UF setCity(e.target.value)} />
Contato setContact(e.target.value)} />
WhatsApp setWhats(e.target.value)} />
E-mail setEmail(e.target.value)} /> {email && !emailValid && E-mail inválido}
Endereço completo setAddress(e.target.value)} />
Categorias de atuação
{categories.map(c => ( ))}
Será cadastrado como Em Avaliação
Cancelar }>Cadastrar fornecedor
), document.body); } (function() { if (document.getElementById('scp-sup-css')) return; const s = document.createElement('style'); s.id = 'scp-sup-css'; s.textContent = ` .scp-sup-list { display: flex; flex-direction: column; } .scp-sup-row { display: grid; grid-template-columns: 42px 1.4fr 1fr auto; gap: 16px; padding: 16px 22px; align-items: center; border-bottom: 1px solid var(--border-soft); transition: background .12s; } .scp-sup-row:hover { background: var(--surface-2); } .scp-sup-row.clickable { cursor: pointer; transition: background .12s; } .scp-sup-row.clickable:focus-visible { outline: none; background: var(--surface-2); box-shadow: inset 3px 0 0 oklch(0.78 0.16 160); } .scp-sup-row:last-child { border-bottom: 0; } .scp-sup-row .info .head { display: flex; align-items: center; gap: 8px; } .scp-sup-row .info .n { font-size: 14px; font-weight: 600; color: var(--fg); } .scp-sup-row .info .meta { display: flex; gap: 16px; flex-wrap: wrap; font-size: 11.5px; color: var(--fg-3); margin-top: 4px; } .scp-sup-row .info .cats { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 8px; } .scp-sup-row .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; min-width: 320px; } .scp-sup-row .stats > div { display: flex; flex-direction: column; gap: 2px; align-items: flex-start; } .scp-sup-row .stats span { font-size: 10px; color: var(--fg-3); text-transform: uppercase; letter-spacing: .08em; font-weight: 600; } .scp-sup-row .stats strong { font-size: 16px; font-weight: 600; color: var(--fg); } .scp-sup-row .stats strong.em { color: oklch(0.92 0.14 160); } .scp-sup-row .stats strong.num { font-family: 'JetBrains Mono', monospace; } .scp-sup-row .acts { display: flex; gap: 4px; } .scp-sup-row.just-added { background: oklch(0.26 0.06 160 / .35); animation: justAddedPulse 2.4s ease-out; } @keyframes justAddedPulse { 0% { background: oklch(0.34 0.10 160 / .55); box-shadow: inset 3px 0 0 oklch(0.78 0.16 160); } 100% { background: oklch(0.205 0.012 250); box-shadow: inset 3px 0 0 transparent; } } .scp-modal-sup { width: 640px; } .scp-modal-sup .scp-modal-body { max-height: 60vh; } .scp-modal-sup .scp-input { width: 100%; } `; document.head.appendChild(s); })(); window.ScreenSuppliers = ScreenSuppliers;