/* global React, Icon, BRL, NUM, Ticker, Drawer */ const PREC_STATUS = { rascunho: { label: 'RASCUNHO', cls: '', dot: 'var(--fg-3)' }, revisao: { label: 'EM REVISÃO', cls: 'badge-amber',dot: 'var(--amber)' }, pronto: { label: 'PRONTO', cls: 'badge-lime', dot: 'var(--lime)' }, enviado: { label: 'ENVIADO', cls: 'badge-cyan', dot: 'var(--cyan)' }, descartado: { label: 'DESCART.', cls: '', dot: 'var(--fg-4)' }, }; function MPrecificacao({ onNav }) { const [precificacoes, setPrecificacoes] = React.useState([]); const [aggregates, setAggregates] = React.useState({}); // { precifId: { custo, margem, itens, alertInner, descart } } const [loading, setLoading] = React.useState(true); const [loadError, setLoadError] = React.useState(null); const [selected, setSelected] = React.useState(null); const [drawerTab, setDrawerTab] = React.useState('resumo'); const [filtroStatus, setFiltroStatus] = React.useState('todos'); const [regimePadrao, setRegimePadrao] = React.useState('lucro-real'); const [importing, setImporting] = React.useState(false); const [syncing, setSyncing] = React.useState(false); const [showNew, setShowNew] = React.useState(false); const [toast, setToast] = React.useState(null); const showToast = (msg, ms = 4000) => { setToast(msg); setTimeout(() => setToast(null), ms); }; const reload = React.useCallback(async () => { try { setLoadError(null); const rows = await window.dataApi.listPrecificacoes(); setPrecificacoes(rows); // Carrega agregados em paralelo (custo total, margem média, alertas) const agg = {}; await Promise.all(rows.map(async (p) => { try { const itens = await window.dataApi.listPrecificacaoItens(p.id); const ativos = itens.filter(i => !i.descartado); const custo = ativos.reduce((s, i) => s + i.custo * i.qtd, 0); const margemMedia = ativos.length > 0 ? ativos.reduce((s, i) => s + i.margem, 0) / ativos.length : 0; agg[p.id] = { custo, margem: margemMedia, itens: ativos.length, descart: itens.filter(i => i.descartado).length, alertInner: itens.filter(i => i.alerta_inner).length, }; } catch { agg[p.id] = { custo: 0, margem: 0, itens: 0, descart: 0, alertInner: 0 }; } })); setAggregates(agg); } catch (e) { setLoadError(e?.message || 'Falha ao carregar precificações'); } finally { setLoading(false); } }, []); React.useEffect(() => { reload(); }, [reload]); // Lista enriquecida com agregados (forma similar ao mock antigo) const lista = React.useMemo(() => precificacoes.map(p => { const a = aggregates[p.id] || { custo: 0, margem: 0, itens: 0, descart: 0, alertInner: 0 }; return { // fields do edital expostos no top level pra UI atual continuar igual id: p.edital?.numero, numero: p.edital?.numero, orgao: p.edital?.orgao, uf: p.edital?.uf, cidade: p.edital?.cidade, plataforma: p.edital?.plataforma, valor: p.edital?.valor || 0, abertura: p.edital?.abertura, itens: p.edital?.itens || 0, // dados da precificação precId: p.id, prec: { status: p.status, regime: p.regime, proposta: p.proposta_gerada, custo: a.custo, margem: a.margem, itens: a.itens, descart: a.descart, alertInner: a.alertInner, }, _raw: p, }; }), [precificacoes, aggregates]); const listaFiltrada = React.useMemo(() => { if (filtroStatus === 'inner') return lista.filter(e => e.prec.alertInner > 0); if (filtroStatus === 'proposta') return lista.filter(e => !e.prec.proposta); return lista; }, [filtroStatus, lista]); const sel = selected ? lista.find(e => e.precId === selected) : null; // KPIs gerais const totalValor = lista.reduce((a, e) => a + (e.valor || 0), 0); const totalCusto = lista.reduce((a, e) => a + (e.prec.custo || 0), 0); const margemMedia = lista.length > 0 ? lista.reduce((a, e) => a + (e.prec.margem || 0), 0) / lista.length : 0; const alertasInner = lista.reduce((a, e) => a + (e.prec.alertInner || 0), 0); const handleImport = () => { if (importing) return; setImporting(true); setTimeout(() => { setImporting(false); const nLinhas = Math.floor(Math.random() * 80) + 40; showToast(`Tabela do fornecedor importada · ${nLinhas} SKUs atualizados · INNER recalculado`); }, 1800); }; const handleSync = () => { if (syncing) return; setSyncing(true); setTimeout(() => { setSyncing(false); showToast('Sincronização concluída · 7 fornecedores · 14 reajustes detectados nos últimos 30d'); }, 2000); }; const handleNewCreated = (id) => { setShowNew(false); showToast(`Nova precificação ${id} criada · status RASCUNHO · cargas tributárias do regime padrão aplicadas`); }; return ( <>
Precificação Blindada · Regra do INNERM03

Precificação de editais

Markup automático de 60% OU R$ 0,01 abaixo do edital (o maior). Regra do INNER protege a margem em triangulação. Preço mínimo exibido como alerta — não trava lance.

{/* KPIs gerais */}
a + e.prec.itens, 0)} itens precificados`} /> 0 ? 'amber' : 'lime'} />
setFiltroStatus('todos')}> Status: TODOS {filtroStatus === 'todos' && }
setFiltroStatus(filtroStatus === 'inner' ? 'todos' : 'inner')}> Apenas com alertas INNER {filtroStatus === 'inner' && }
setFiltroStatus(filtroStatus === 'proposta' ? 'todos' : 'proposta')}> Proposta pendente {filtroStatus === 'proposta' && }
Regime tributário padrão
{[ { id: 'lucro-real', l: 'LUCRO REAL' }, { id: 'simples', l: 'SIMPLES' }, { id: 'presumido', l: 'PRESUMIDO' }, ].map(r => (
{ setRegimePadrao(r.id); showToast(`Regime padrão alterado para ${r.l} · novas precificações usarão este regime`); }}>{r.l}
))}
{loadError && (
{loadError}
)}
{loading ? 'Carregando precificações...' : `${listaFiltrada.length} editais`} {filtroStatus !== 'todos' && !loading && (de {lista.length})}
{loading ? '...' : filtroStatus === 'inner' ? 'apenas com alertas INNER' : filtroStatus === 'proposta' ? 'apenas com proposta ainda não gerada' : `${lista.filter(e => e.prec.status === 'pronto').length} prontos · ${lista.filter(e => e.prec.status === 'revisao').length} em revisão · ${lista.filter(e => e.prec.status === 'enviado').length} enviados`}
{loading && ( )} {!loading && listaFiltrada.length === 0 && ( )} {!loading && listaFiltrada.map(e => { const st = PREC_STATUS[e.prec.status] || PREC_STATUS.rascunho; return ( { setSelected(e.precId); setDrawerTab('resumo'); }} style={{ cursor: 'pointer' }}> ); })}
Edital Órgão / Plataforma Vlr. ref. Custo BTM Itens prec. Margem INNER Regime Status Proposta
Carregando precificações do Supabase...
{lista.length === 0 ? 'Nenhuma precificação cadastrada.' : 'Nenhum edital corresponde a este filtro.'}
{e.numero}
{new Date(e.abertura).toLocaleDateString('pt-BR')}
{e.orgao}
{e.plataforma} · {e.uf}
{BRL(e.valor)} {BRL(e.prec.custo)} {e.prec.itens} / {e.itens} {e.prec.descart > 0 &&
{e.prec.descart} descart.
}
= 35 ? 'var(--lime)' : e.prec.margem >= 25 ? 'var(--amber)' : 'var(--red)' }}>
= 35 ? 'var(--lime)' : e.prec.margem >= 25 ? 'var(--amber)' : 'var(--red)', fontWeight: 600 }}>{e.prec.margem.toFixed(1)}%
{e.prec.alertInner > 0 ? ( {e.prec.alertInner} ) : ( )} {e.prec.regime === 'lucro-real' ? 'LUCRO REAL' : 'SIMPLES'} {st.label} {e.prec.proposta ? ( PDF ) : ( )}
{sel && setSelected(null)} tab={drawerTab} onTab={setDrawerTab} onNav={onNav} />} {showNew && setShowNew(false)} onCreate={handleNewCreated} />} {toast && (
{toast}
)} ); } function NovaPrecificacaoModal({ regimePadrao, onClose, onCreate }) { const [form, setForm] = React.useState({ id: 'PE-' + String(Math.floor(Math.random() * 900) + 100) + '/2026', orgao: '', valor: '', itens: '', regime: regimePadrao, base: 'manual', }); const [erro, setErro] = React.useState(null); const set = (k, v) => setForm({ ...form, [k]: v }); const submit = () => { if (!form.id || !form.orgao || !form.valor || !form.itens) { setErro('Preencha ID, Órgão, Valor e Quantidade de itens'); return; } onCreate(form.id); }; return (
e.stopPropagation()}>
Nova precificação manual
Quando usar isso
Para precificar editais que vieram fora do fluxo automático (e-mail direto, indicação, conversão de PDV em licitação). A planilha será gerada vazia para você preencher item a item.
ID DO EDITAL *
set('id', e.target.value)} />
BASE
{[ { id: 'manual', l: 'EM BRANCO' }, { id: 'modelo', l: 'COPIAR MODELO' }, ].map(o => (
set('base', o.id)}>{o.l}
))}
ÓRGÃO *
set('orgao', e.target.value)} placeholder="Ex: Prefeitura Municipal de Niterói" />
VALOR DE REFERÊNCIA (R$) *
set('valor', e.target.value)} placeholder="482350" />
QTD DE ITENS *
set('itens', e.target.value)} placeholder="47" />
REGIME TRIBUTÁRIO
{[ { id: 'lucro-real', l: 'LUCRO REAL' }, { id: 'simples', l: 'SIMPLES' }, { id: 'presumido', l: 'PRESUMIDO' }, ].map(r => (
set('regime', r.id)}>{r.l}
))}
{erro && (
{erro}
)}
); } /* ── PRECIFICACAO DRAWER ───────────────────────────────────── */ function PrecDrawer({ edital, onClose, tab, onTab, onNav }) { const [itens, setItens] = React.useState(null); React.useEffect(() => { let mounted = true; if (!edital?.precId) return; window.dataApi.listPrecificacaoItens(edital.precId) .then(rows => { if (mounted) setItens(rows); }) .catch(() => { if (mounted) setItens([]); }); return () => { mounted = false; }; }, [edital?.precId]); const st = PREC_STATUS[edital.prec.status] || PREC_STATUS.rascunho; return ( Precificação · {edital.prec.regime === 'lucro-real' ? 'LUCRO REAL' : edital.prec.regime === 'simples' ? 'SIMPLES' : 'PRESUMIDO'}M03{st.label}} title={{edital.numero} · {edital.orgao}} subtitle={`${edital.cidade || '—'} · ${edital.uf} · ${edital.itens} itens · ref. ${BRL(edital.valor)}`} headRight={
{edital.prec.alertInner > 0 && {edital.prec.alertInner} INNER}
} tabs={[ { id: 'resumo', label: 'Resumo' }, { id: 'planilha', label: 'Planilha', count: itens?.length ?? '…' }, { id: 'cargas', label: 'Cargas tributárias' }, { id: 'frete', label: 'Frete por região' }, { id: 'proposta', label: 'Proposta' }, ]} activeTab={tab} onTabChange={onTab} footer={ <> {edital.prec.proposta ? ( ) : ( )} } > {tab === 'resumo' && } {tab === 'planilha' && } {tab === 'cargas' && } {tab === 'frete' && } {tab === 'proposta' && }
); } /* ── TAB CONTENT ──────────────────────────────────────────── */ function TabResumo({ edital, itens }) { return (
KPIs DA PRECIFICAÇÃO
= 35 ? 'lime' : 'amber'} /> 0 ? 'amber' : 'lime'} />
REGIME TRIBUTÁRIO
SELECIONADO
{edital.prec.regime === 'lucro-real' ? 'LUCRO REAL' : 'SIMPLES NACIONAL'}
{edital.prec.regime === 'lucro-real' ? 'ICMS 7% · PIS 1,65% · COFINS 7,6%' : 'Anexo I · Alíquota efetiva 13,20%'}
MARKUP
60% sobre custo total
ou R$ 0,01 abaixo do edital (o maior)
STATUS DA PROPOSTA
{edital.prec.proposta ? 'GERADA' : 'PENDENTE'}
{edital.prec.proposta ? 'pronta para envio' : `gerar antes da abertura (${new Date(edital.abertura).toLocaleDateString('pt-BR')})`}
RESUMO DE COBERTURA
0 ? 'text-red' : ''} />
{edital.prec.alertInner > 0 && (
{edital.prec.alertInner} {edital.prec.alertInner === 1 ? 'item tem' : 'itens têm'} risco de prejuízo em triangulação INNER
Veja na aba Planilha — itens com sobra de embalagem maior que 50% do pedido.
)}
); } function TabPlanilha({ edital, itens }) { if (itens === null) { return
Carregando planilha...
; } if (itens.length === 0) { return
Nenhum item precificado. Cadastre itens no edital antes (M01).
; } const cargaPct = edital.prec.regime === 'lucro-real' ? 16.25 : 13.20; return (
{itens.length} de {edital.itens} itens · regime {edital.prec.regime === 'lucro-real' ? 'LUCRO REAL' : edital.prec.regime === 'simples' ? 'SIMPLES' : 'PRESUMIDO'} · markup {edital._raw?.markup_pct || 60}%
{itens.map(it => { const blocos = it.inner > 0 ? Math.ceil(it.qtd / it.inner) : 1; const qtdComprar = blocos * it.inner; const sobra = qtdComprar - it.qtd; const cargas = it.custo * cargaPct / 100 + it.frete; const totalLance = it.preco_lance * it.qtd; const danger = it.alerta_inner; return ( ); })}
# Descrição Qtd ed. Inner A comprar Custo +Cargas Edital Lance Margem Total
{String(it.idx).padStart(2, '0')}
{it.desc}
{it.cod &&
{it.cod}
} {danger && (
sem produto cadastrado / cruzamento INNER falhou
)}
{NUM(it.qtd)} {it.inner}
{NUM(qtdComprar)}
{sobra > 0 &&
+{sobra}
}
{BRL(it.custo).replace('R$ ', '')} {BRL(cargas).replace('R$ ', '')} {BRL(it.edital_unit).replace('R$ ', '')} {BRL(it.preco_lance).replace('R$ ', '')} 35 ? 'var(--lime)' : it.margem > 20 ? 'var(--amber)' : 'var(--red)' }}>{it.margem.toFixed(1)}% {BRL(totalLance).replace('R$ ', '')}
REGRA DO INNER
Embalagem mínima de venda do fornecedor é obrigatória no cadastro de custo — protege contra prejuízo em triangulação.
✓ Aprovado · qtd. licitada ≥ múltiplo do INNER.
⚠ Sobra · arredonda pro próximo bloco e calcula prejuízo no excedente.
✕ Crítico · sem produto cadastrado.
); } function TabCargas({ edital }) { const isLucroReal = edital.prec.regime === 'lucro-real'; return (
Configuração de Cargas Tributárias
{isLucroReal ? 'LUCRO REAL' : 'SIMPLES'}
SIMPLES NACIONAL
ATIVO
INATIVO
BTM opta pelo {isLucroReal ? 'Lucro Real' : 'Simples Nacional'}. Caso futuro fornecedor seja Simples, o sistema aplica alíquotas próprias.
ANEXO DO SIMPLES (Comércio)
ANEXO I
ANEXO II
ANEXO III
Cálculo automático ativo · sistema escolhe regime mais vantajoso por item.
); } function TabFrete() { const regioes = [ { uf: 'RJ', cidade: 'Niterói', tipo: 'CORREIOS PAC', kg: 12.50, prazo: '3 dias', total: 312.40 }, { uf: 'SP', cidade: 'São Paulo', tipo: 'CORREIOS SEDEX', kg: 28.40, prazo: '2 dias', total: 612.80 }, { uf: 'MG', cidade: 'Belo Horizonte', tipo: 'TRANSP. BRASPRESS', kg: 18.20, prazo: '4 dias', total: 487.30 }, { uf: 'ES', cidade: 'Vitória', tipo: 'CORREIOS PAC', kg: 22.10, prazo: '5 dias', total: 524.10 }, { uf: 'SC', cidade: 'Joinville', tipo: 'TRANSP. JADLOG', kg: 32.40, prazo: '6 dias', total: 712.90 }, { uf: 'PR', cidade: 'Curitiba', tipo: 'TRANSP. BRASPRESS', kg: 30.20, prazo: '6 dias', total: 684.20 }, { uf: 'RS', cidade: 'Porto Alegre', tipo: 'TRANSP. JADLOG', kg: 38.40, prazo: '7 dias', total: 882.40 }, ]; return (
Tabela de Frete por Região · automática na precificação
{regioes.map((r, i) => ( ))}
UF Destino Modal R$ / Kg Prazo Total estimado
{r.uf} {r.cidade} {r.tipo} {BRL(r.kg).replace('R$ ', '')} {r.prazo} {BRL(r.total)}
); } function TabProposta({ edital, itens }) { if (!edital.prec.proposta) { return (
Proposta ainda não gerada
Conclua a precificação (status: PRONTO) e gere o PDF da proposta com timbre BTM para envio à plataforma.
); } return (
Proposta_BTM_{(edital.numero || '').replace('/', '_')}.pdf
3 págs · 187 KB
BTM
BTM SERVIÇOS E COMÉRCIO LTDA
CNPJ 33.804.351/0001-04 · Rio de Janeiro/RJ
PROPOSTA DE PREÇOS
Pregão Eletrônico nº {(edital.numero || '').replace('PE-', '')} — {edital.orgao}
{[ { d: 'Papel A4 75g — Resma 500 fls', q: 2400, u: 21.49, t: 51576.00 }, { d: 'Caneta esferográfica azul 1.0mm', q: 4800, u: 1.44, t: 6912.00 }, { d: 'Pasta AZ ofício preta', q: 240, u: 13.39, t: 3213.60 }, { d: 'Grampeador 26/6 médio', q: 60, u: 34.89, t: 2093.40 }, { d: 'Marcador permanente preto', q: 96, u: 5.39, t: 517.44 }, ].map((r, i) => ( ))}
# Descrição Qtd Unit Total
{String(i + 1).padStart(2, '0')} {r.d} {r.q.toLocaleString('pt-BR')} {r.u.toFixed(2)} {r.t.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
VALOR TOTAL {BRL(edital.valor * 0.85)}
Validade: 60 dias · Prazo de entrega: 30 dias úteis · Frete CIF
Validações
{[ 'Papel timbrado BTM aplicado', 'Cabeçalho com CNPJ correto', 'Validade 60 dias (compatível)', 'Prazo de entrega 30d (compatível)', `${edital.prec.itens} itens precificados · ${edital.prec.descart} descartados`, `Margem total ${edital.prec.margem.toFixed(1)}% ≥ 35% (mínimo)`, ].map(v => (
{v}
))}
); } /* ── REUSABLE INTERNALS ────────────────────────────────────── */ function PrecKpi({ label, value, sub, cls }) { return (
{label}
{value}
{sub}
); } function CargaInput({ label, value, sufix, prefix, sub }) { return (
{label}
{prefix && {prefix}} {sufix && {sufix}}
{sub &&
{sub}
}
); } function Field({ label, value, valueClass }) { return (
{label}
{value}
); } window.MPrecificacao = MPrecificacao;