/* global React, Icon, BRL, NUM, KANBAN_EMP, Ticker, Drawer */ const STATUS_ORDER = KANBAN_EMP.map(s => s.id); const STATUS_LABEL = Object.fromEntries(KANBAN_EMP.map(s => [s.id, s.label])); function MEmpenhos({ onNav }) { const [empenhos, setEmpenhos] = React.useState([]); const [loading, setLoading] = React.useState(true); const [view, setView] = React.useState('kanban'); const [selected, setSelected] = React.useState(null); const [drawerTab, setDrawerTab] = React.useState('pipe'); const [capturing, setCapturing] = React.useState(false); const [showLancar, setShowLancar] = 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 { const rows = await window.dataApi.listEmpenhos(); setEmpenhos(rows); } catch { setEmpenhos([]); } finally { setLoading(false); } }, []); React.useEffect(() => { reload(); }, [reload]); const sel = selected ? empenhos.find(e => e.id === selected) : null; const openEmp = (id) => { setSelected(id); setDrawerTab('pipe'); }; const handleCapturar = () => { if (capturing) return; setCapturing(true); setTimeout(() => { setCapturing(false); const novos = Math.floor(Math.random() * 3) + 1; showToast(`Caixa de entrada varrida · ${novos} ${novos === 1 ? 'empenho novo capturado' : 'empenhos novos capturados'} · IA validou contra lances vencedores`); }, 2000); }; const handleLancado = async (form) => { try { const created = await window.dataApi.insertEmpenho(form); await reload(); setShowLancar(false); showToast(`Empenho ${created.numero} lançado · status RECEBIDO`); } catch (e) { showToast(`Erro: ${e?.message || e}`, 6000); } }; const stats = { hoje: empenhos.filter(e => e.dias === 0).length, aguardNF: empenhos.filter(e => ['val', 'nf-sol'].includes(e.status)).length, aguardNFTotal: empenhos.filter(e => ['val', 'nf-sol'].includes(e.status)).reduce((a, e) => a + e.valor, 0), transito: empenhos.filter(e => e.status === 'transp').length, atrasados: empenhos.filter(e => e.atraso).length, atrasadosTotal: empenhos.filter(e => e.atraso).reduce((a, e) => a + e.valor, 0), hojeTotal: empenhos.filter(e => e.dias <= 2).reduce((a, e) => a + e.valor, 0), }; return ( <>
Gestão de EmpenhosM05

{loading ? 'Carregando...' : `${empenhos.length} empenhos ativos · ${BRL(empenhos.reduce((a, e) => a + e.valor, 0))} em carteira`}

Captura automática · validação contra lance · 12 status com movimentação automática · resposta ao órgão em cada etapa.

setView('kanban')}> KANBAN
setView('list')}> LISTA
e.dias <= 2).length} sub={`+${BRL(stats.hojeTotal)}`} />
{view === 'kanban' && } {view === 'list' && }
{sel && setSelected(null)} tab={drawerTab} onTab={setDrawerTab} onNav={onNav} />} {showLancar && setShowLancar(false)} onLancar={handleLancado} />} {toast && (
{toast}
)} ); } function LancarEmpenhoModal({ onClose, onLancar }) { const [form, setForm] = React.useState({ id: '2026NE' + String(Math.floor(Math.random() * 9000) + 1000).padStart(6, '0'), orgao: '', edital: '', arp: '', valor: '', email: '', uf: 'RJ', triangulacao: false, }); const [erro, setErro] = React.useState(null); const set = (k, v) => setForm({ ...form, [k]: v }); const [saving, setSaving] = React.useState(false); const submit = async () => { if (!form.orgao || !form.valor) { setErro('Preencha Órgão e Valor'); return; } setErro(null); setSaving(true); try { await onLancar({ numero: form.id, orgao: form.orgao, orgaoFull: form.orgao, uf: form.uf, valor: form.valor, email: form.email, arp: form.arp, triangulacao: form.triangulacao, }); } catch (e) { setErro(e?.message || 'Erro ao salvar'); } finally { setSaving(false); } }; return (
e.stopPropagation()}>
Lançar empenho manualmente
Quando usar isso
Para empenhos recebidos fora do fluxo automático (cópia física, telefone, indicação). Após salvar, o pipeline de 12 etapas começa em RECEBIDO e a IA valida contra os lances vencedores.
NÚMERO DO EMPENHO *
set('id', e.target.value)} placeholder="2026NE000XXX" />
UF
ÓRGÃO *
set('orgao', e.target.value)} placeholder="Ex: Prefeitura Municipal de Niterói" />
EDITAL DE ORIGEM *
set('edital', e.target.value)} placeholder="PE-187/2026" />
ARP (opcional)
set('arp', e.target.value)} placeholder="ARP nº 014/2026" />
VALOR DO EMPENHO (R$) *
set('valor', e.target.value)} placeholder="51576.60" />
E-MAIL DO ÓRGÃO (opcional)
set('email', e.target.value)} placeholder="compras@orgao.gov.br" />
EMPENHO COM TRIANGULAÇÃO
Marque se algum item será comprado direto do fornecedor (não passa pelo estoque BTM).
set('triangulacao', !form.triangulacao)}>
{erro && (
{erro}
)}
); } function KpiSmall({ l, v, sub, cls }) { return (
{l}
{v} {sub}
); } function EmpenhoKanban({ onSelect, empenhos, loading }) { if (loading) return
Carregando empenhos...
; return (
{KANBAN_EMP.map((col, idx) => { const itens = empenhos.filter(e => e.status === col.id); return (
{String(idx + 1).padStart(2, '0')} {col.label} {itens.length}
{itens.map(e => onSelect(e.id)} />)}
); })}
); } function EmpKanCard({ e, onClick }) { return (
{e.numero}
{e.orgao}
{e.dias}d {e.prazo !== undefined && <>·{e.prazo < 0 ? `${Math.abs(e.prazo)}d atraso` : `${e.prazo}d`}} {BRL(e.valor)}
{e.rastreio && (
📦 {e.rastreio}
)}
); } function EmpenhoLista({ onSelect, empenhos, loading }) { return (
{loading && ( )} {!loading && empenhos.length === 0 && ( )} {!loading && empenhos.map(e => ( onSelect(e.id)} style={{ cursor: 'pointer' }}> ))}
Empenho Órgão UF Valor Status Dias Prazo NF Edital
Carregando empenhos...
Nenhum empenho cadastrado.
{e.numero} {e.orgao} {e.uf} {BRL(e.valor)} {STATUS_LABEL[e.status]} {e.dias}d {e.prazo === undefined ? '—' : e.prazo < 0 ? `${Math.abs(e.prazo)}d atraso` : `${e.prazo}d`} {e.nf || '—'} {e.edital}
); } /* ── EMPENHO DRAWER ────────────────────────────────────────── */ function EmpenhoDrawer({ empenho, onClose, tab, onTab, onNav }) { const docCount = empenho.status === 'val' ? 2 : empenho.status === 'nf-emit' || empenho.status === 'compra' ? 4 : 5; const statusBadgeCls = empenho.atraso ? 'badge-red' : ['pago', 'entreg'].includes(empenho.status) ? 'badge-lime' : ['val', 'rec'].includes(empenho.status) ? 'badge-cyan' : 'badge-amber'; return ( EmpenhoM05{STATUS_LABEL[empenho.status]}{empenho.atraso && ATRASADO}} title={{empenho.numero} · {empenho.orgao}} subtitle={`${empenho.orgaoFull} · ${empenho.uf} · ${empenho.arp} (${empenho.edital})`} headRight={
VALOR
{BRL(empenho.valor)}
} tabs={[ { id: 'pipe', label: 'Pipeline', count: STATUS_ORDER.indexOf(empenho.status) + 1 + '/' + STATUS_ORDER.length }, { id: 'itens', label: 'Itens & E-mail' }, { id: 'docs', label: 'Documentos', count: docCount }, { id: 'resp', label: 'Respostas' }, { id: 'ia', label: 'IA & Ações' }, ]} activeTab={tab} onTabChange={onTab} footer={ <> {empenho.status === 'val' && } {empenho.status === 'transp' && } {empenho.status === 'cobr' && } {!['val', 'transp', 'cobr'].includes(empenho.status) && } } > {tab === 'pipe' && } {tab === 'itens' && } {tab === 'docs' && } {tab === 'resp' && } {tab === 'ia' && }
); } function TabPipeline({ empenho }) { const currentIdx = STATUS_ORDER.indexOf(empenho.status); const baseTimes = { rec: { dt: '12/05 08:42', actor: empenho.email }, val: { dt: '12/05 08:44', actor: 'IA Gerente · auto' }, 'nf-sol': { dt: '12/05 09:12', actor: 'Sistema → escritório@contabilidade.com' }, 'nf-emit': { dt: '12/05 14:08', actor: empenho.nf && empenho.nf !== '—' && empenho.nf !== 'solicitada' ? `NF ${empenho.nf} · NFE.io` : 'aguardando' }, compra: { dt: '13/05', actor: 'Suzano Distribuição' }, separ: { dt: '14/05', actor: 'Almox. BTM' }, rev: { dt: '14/05', actor: 'Conferência interna' }, exp: { dt: '15/05', actor: 'Expedição' }, transp: { dt: '16/05', actor: empenho.rastreio || 'Correios/SSW' }, entreg: { dt: '20/05', actor: empenho.orgao }, cobr: { dt: empenho.atraso ? `${empenho.dias}d pós-NF` : '—', actor: empenho.atraso ? 'Financeiro · cobrança ativa' : 'aguardando' }, pago: { dt: empenho.status === 'pago' ? '02/05' : '—', actor: empenho.status === 'pago' ? 'Banco do Brasil' : 'aguardando' }, }; return (
{STATUS_ORDER.map((sid, i) => { const done = i < currentIdx; const current = i === currentIdx; const future = i > currentIdx; const label = STATUS_LABEL[sid]; const info = baseTimes[sid] || {}; return (
{current &&
}
{done ? : current ? : null}
{i < STATUS_ORDER.length - 1 &&
}
{label}
{future ? '—' : info.dt}
{info.actor && !future &&
{info.actor}
}
); })}
); } function TabItensEmail({ empenho }) { const itens = [ { d: 'Papel A4 75g — Resma 500 fls', c: 'BTM-PA-001', q: 2400, u: 21.49, t: 51576.00, cen: 'ESTOQUE' }, { d: 'Caneta esfer. azul 1.0mm', c: 'BTM-CN-014', q: 4800, u: 1.44, t: 6912.00, cen: 'ESTOQUE' }, ...(empenho.triangulacao ? [{ d: 'Pasta AZ ofício preta', c: 'BTM-PT-052', q: 240, u: 13.39, t: 3213.60, cen: 'TRIANGULAÇÃO' }] : []), ]; const total = itens.reduce((a, i) => a + i.t, 0); return (
ITENS DO EMPENHO ({itens.length})
VALIDADO ✓ LANCE
{itens.map((it, i) => ( ))}
DescriçãoCodQtdUnitTotalCenário
{it.d} {it.c} {NUM(it.q)} {BRL(it.u)} {BRL(it.t)} {it.cen}
Total: {BRL(total)}
E-MAIL ORIGINAL CAPTURADO
VALIDADO
De: {empenho.email}
Para: contato@btmsigma.com
Assunto: Empenho {empenho.numero} (Nomenclatura detectada: EMPENHO)
Anexos: PDF NE_{(empenho.numero || '').replace('2026NE', '')}_{empenho.orgao.replace(/[^A-Z]/g, '')}.pdf
Prezados, encaminhamos a nota de empenho referente à {empenho.arp}, originária do pregão {empenho.edital}. Solicitamos a entrega no Almoxarifado Central conforme cronograma anexo. Atenciosamente, Setor de Compras — {empenho.orgaoFull}.
); } function TabDocs({ empenho }) { const currentIdx = STATUS_ORDER.indexOf(empenho.status); const docs = [ { nome: `NE_${(empenho.numero || '').replace('2026NE', '')}.pdf`, ext: 'pdf', tamanho: '184 KB', published: 'há ' + empenho.dias + 'd', available: true, cat: 'EMPENHO' }, { nome: `NF_${empenho.nf || '—'}_BTM.pdf`, ext: 'pdf', tamanho: '218 KB', published: empenho.nf && empenho.nf !== '—' && empenho.nf !== 'solicitada' ? 'há ' + Math.max(0, empenho.dias - 1) + 'd' : '—', available: empenho.nf && empenho.nf !== '—' && empenho.nf !== 'solicitada', cat: 'NF' }, { nome: `NFe_${empenho.nf || '—'}.xml`, ext: 'xml', tamanho: '12 KB', published: empenho.nf && empenho.nf !== '—' && empenho.nf !== 'solicitada' ? 'há ' + Math.max(0, empenho.dias - 1) + 'd' : '—', available: empenho.nf && empenho.nf !== '—' && empenho.nf !== 'solicitada', cat: 'NFe' }, { nome: `Pedido_Compras_${(empenho.numero || '').slice(-4)}.pdf`, ext: 'pdf', tamanho: '94 KB', published: currentIdx >= STATUS_ORDER.indexOf('compra') ? 'há ' + Math.max(0, empenho.dias - 2) + 'd' : '—', available: currentIdx >= STATUS_ORDER.indexOf('compra'), cat: 'COMPRA' }, { nome: 'Romaneio.pdf', ext: 'pdf', tamanho: '76 KB', published: currentIdx >= STATUS_ORDER.indexOf('exp') ? 'há ' + Math.max(0, empenho.dias - 7) + 'd' : '—', available: currentIdx >= STATUS_ORDER.indexOf('exp'), cat: 'ROMANEIO' }, ...(empenho.rastreio ? [{ nome: `Etiqueta_${empenho.rastreio}.pdf`, ext: 'pdf', tamanho: '52 KB', published: 'há ' + Math.max(0, empenho.dias - 10) + 'd', available: true, cat: 'RASTREIO' }] : []), ...(currentIdx >= STATUS_ORDER.indexOf('entreg') ? [{ nome: 'Comprovante_Entrega.pdf', ext: 'pdf', tamanho: '184 KB', published: 'há ' + Math.max(0, empenho.dias - 14) + 'd', available: true, cat: 'ENTREGA' }] : []), ]; return (
DOCUMENTOS DO EMPENHO
{docs.map((d, i) => (
{d.ext.toUpperCase()}
{d.nome}
{d.tamanho} · {d.published === '—' ? 'aguardando' : 'publicado ' + d.published}
{d.cat} {d.available ? 'pronto' : 'aguardando'}
))}
); } function TabRespostas({ empenho }) { const currentIdx = STATUS_ORDER.indexOf(empenho.status); const resps = [ { stage: 'Recebimento confirmado', time: '12/05 08:46', body: `Acusamos o recebimento do empenho ${empenho.numero} e estamos providenciando o atendimento.`, threshold: 'rec' }, { stage: 'NF emitida', time: '12/05 14:11', body: `Sua NF nº ${empenho.nf || '—'} foi emitida e segue anexa. Iniciaremos a separação.`, threshold: 'nf-emit' }, { stage: 'Em separação', time: '14/05 09:30', body: 'Pedido em separação no almoxarifado. Confirmaremos despacho em até 48h.', threshold: 'separ' }, { stage: 'Despacho + rastreio', time: empenho.rastreio ? '16/05 11:42' : 'aguardando', body: empenho.rastreio ? `Pedido despachado · rastreio ${empenho.rastreio} · prazo de entrega 5-7 dias úteis.` : null, threshold: 'transp' }, { stage: 'Comprovante de entrega', time: currentIdx >= STATUS_ORDER.indexOf('entreg') ? '20/05 14:15' : 'aguardando', body: currentIdx >= STATUS_ORDER.indexOf('entreg') ? 'Mercadoria entregue conforme protocolo. Comprovante anexo.' : null, threshold: 'entreg' }, { stage: 'Cobrança (40d pós-NF)', time: empenho.atraso ? `D+${empenho.dias}` : 'aguardando', body: empenho.atraso ? 'Notificação de cobrança encaminhada ao setor financeiro do órgão.' : null, threshold: 'cobr' }, ]; return (
RESPOSTAS AUTOMÁTICAS AO ÓRGÃO
{resps.map((r, i) => { const ok = currentIdx >= STATUS_ORDER.indexOf(r.threshold); return (
{ok && }
{r.stage}
{r.body &&
"{r.body}"
}
{r.time}
); })}
); } function TabIaAcoes({ empenho, onNav }) { return (
IA · Sugestão
{empenho.triangulacao ? ( <>Identifiquei que Pasta AZ é triangulação. Posso emitir Pedido de Compras à Polibras automaticamente com INNER aplicado (20 un · qtd 240 = 12 inners exatos · sem sobra)? ) : empenho.atraso ? ( <>Empenho com {empenho.dias} dias pós-emissão. Posso iniciar protocolo de cobrança automática — disparar e-mail para {empenho.email} e abrir ticket no setor financeiro? ) : empenho.status === 'val' ? ( <>Empenho validado contra lance. Posso seguir o fluxo automático: solicitar NF → gerar pedido de compras → cronograma de expedição. Tudo respeitando os prazos do órgão. ) : ( <>Empenho em {STATUS_LABEL[empenho.status]}. Posso preparar a próxima etapa do pipeline automaticamente assim que houver confirmação da etapa atual. )}
PRÓXIMAS AÇÕES
); } window.MEmpenhos = MEmpenhos;