/* global React, Icon, Ticker, Drawer, BRL */ const OCR_STATUS = { concluido: { label: 'CONCLUÍDO', cls: 'badge-lime', dot: 'var(--lime)' }, processando: { label: 'PROCESSANDO', cls: 'badge-cyan', dot: 'var(--cyan)' }, pendencia: { label: 'PENDÊNCIA', cls: 'badge-amber', dot: 'var(--amber)' }, fila: { label: 'EM FILA', cls: '', dot: 'var(--fg-3)' }, }; function MOcr({ onNav }) { const [editais, setEditais] = React.useState([]); const [loading, setLoading] = React.useState(true); const [selected, setSelected] = React.useState(null); const [drawerTab, setDrawerTab] = React.useState('pdf'); const [filtroStatus, setFiltroStatus] = React.useState('todos'); const [reprocessing, setReprocessing] = React.useState(false); const [batching, setBatching] = React.useState(false); const [showUpload, setShowUpload] = React.useState(false); const [toast, setToast] = React.useState(null); const [analiseTarget, setAnaliseTarget] = React.useState(null); const showToast = (msg, ms = 4000) => { setToast(msg); setTimeout(() => setToast(null), ms); }; // Atualiza o edital em memória com a análise recém-salva no banco. const applyAnalise = (realId, analise) => { setEditais(prev => prev.map(e => (e.id === realId ? { ...e, analise_ia: analise } : e))); }; React.useEffect(() => { let mounted = true; window.dataApi.listEditais() .then(rows => { if (mounted) { setEditais(rows); setLoading(false); } }) .catch(() => { if (mounted) setLoading(false); }); return () => { mounted = false; }; }, []); // Status do edital derivado da análise de IA (analise_ia), quando existir. const lista = React.useMemo(() => editais.map(e => { const analise = e.analise_ia || null; const hab = analise && Array.isArray(analise.habilitacao) ? analise.habilitacao : []; const pend = analise && Array.isArray(analise.alertas) ? analise.alertas.filter(a => a.tom === 'red').length : 0; return { ...e, realId: e.id, // uuid real (e.id é sobrescrito por numero abaixo p/ exibição) id: e.numero, analise, ocrData: { ocr: analise ? 'concluido' : 'fila', extracao: analise ? 100 : 0, paginas: 0, checklist: { ok: hab.length, total: hab.length }, zip: false, pendencias: pend, }, }; }), [editais]); const listaFiltrada = React.useMemo(() => { if (filtroStatus === 'pendencia') return lista.filter(e => e.ocrData.pendencias > 0 || e.ocrData.checklist.ok < e.ocrData.checklist.total || e.ocrData.ocr === 'pendencia'); if (filtroStatus === 'zip') return lista.filter(e => e.ocrData.zip); return lista; }, [filtroStatus, lista]); const sel = selected ? lista.find(e => e.id === selected) : null; const openEdital = (id) => { setSelected(id); setDrawerTab('pdf'); }; const handleReprocess = () => { if (reprocessing) return; setReprocessing(true); setTimeout(() => { setReprocessing(false); showToast(`OCR reprocessado · ${lista.length} editais analisados · 23 campos atualizados em média`); }, 2200); }; const handleBatchZip = () => { if (batching) return; const elegiveis = lista.filter(e => e.ocrData.checklist.ok === e.ocrData.checklist.total && !e.ocrData.zip); if (elegiveis.length === 0) { showToast('Nenhum edital elegível para gerar ZIP em lote · habilitação completa + ZIP ainda não gerado'); return; } setBatching(true); setTimeout(() => { setBatching(false); showToast(`${elegiveis.length} ZIPs de habilitação gerados em lote · disponíveis nas pastas dos editais`); }, 2000); }; return ( <>
OCR & Análise InteligenteM02IA ATIVA · análise por texto/PDF

Editais em análise automática

Leitura óptica de editais, extração estruturada de campos (objeto, datas, regiões, prazos), checklist de habilitação (Lei 14.133/2021) e compactação automática do ZIP de habilitação.

setFiltroStatus('todos')}> Status: TODOS {filtroStatus === 'todos' && }
setFiltroStatus(filtroStatus === 'pendencia' ? 'todos' : 'pendencia')}> Apenas com pendência {filtroStatus === 'pendencia' && }
setFiltroStatus(filtroStatus === 'zip' ? 'todos' : 'zip')}> ZIP pronto {filtroStatus === 'zip' && }
Análise IA ativa
{listaFiltrada.length} editais {filtroStatus !== 'todos' && (de {lista.length})}
{filtroStatus === 'pendencia' ? 'apenas com pendência (OCR ou habilitação)' : filtroStatus === 'zip' ? 'ZIP de habilitação pronto' : `${lista.filter(e => e.ocrData.ocr === 'concluido').length} concluídos · ${lista.filter(e => e.ocrData.ocr === 'pendencia').length} com pendência`}
{listaFiltrada.length === 0 && ( )} {listaFiltrada.map(e => { const st = OCR_STATUS[e.ocrData.ocr]; const chkOk = e.ocrData.checklist.ok === e.ocrData.checklist.total; return ( openEdital(e.id)} style={{ cursor: 'pointer' }}> ); })}
Edital Órgão / Plataforma Páginas Status OCR Extração Habilitação ZIP Pendências
Nenhum edital corresponde a este filtro.
{e.id}
{new Date(e.abertura).toLocaleDateString('pt-BR')}
{e.orgao}
{e.plataforma} · {e.uf}
{e.ocrData.paginas || '—'} {st.label}
= 95 ? 'var(--lime)' : e.ocrData.extracao >= 70 ? 'var(--amber)' : 'var(--cyan)' }}>
{e.ocrData.extracao}%
{e.ocrData.checklist.ok}/{e.ocrData.checklist.total} {e.ocrData.zip ? ( PRONTO ) : ( )} {e.ocrData.pendencias > 0 ? ( {e.ocrData.pendencias} aberta{e.ocrData.pendencias > 1 ? 's' : ''} ) : ( )}
{sel && setSelected(null)} tab={drawerTab} onTab={setDrawerTab} onNav={onNav} onAnalisar={() => setAnaliseTarget(sel)} />} {showUpload && setShowUpload(false)} onDone={(nome) => { setShowUpload(false); showToast(`PDF ${nome} enviado · OCR iniciado · resultado em até 5 min`); }} />} {analiseTarget && ( setAnaliseTarget(null)} onDone={(analise) => { applyAnalise(analiseTarget.realId, analise); setAnaliseTarget(null); showToast(`Edital ${analiseTarget.id} analisado pela IA · campos atualizados`); }} /> )} {toast && (
{toast}
)} ); } function UploadPdfModal({ onClose, onDone }) { const [file, setFile] = React.useState(null); const [origem, setOrigem] = React.useState('email'); const [observacao, setObservacao] = React.useState(''); const [uploading, setUploading] = React.useState(false); const submit = () => { if (!file) return; setUploading(true); setTimeout(() => onDone(file), 1500); }; return (
e.stopPropagation()}>
Enviar PDF de edital manualmente
OCR + extração automática
Após o upload, a IA executa OCR completo, extrai 23 campos estruturados e gera checklist de habilitação.
ARQUIVO PDF DO EDITAL *
document.getElementById('ocr-file-input').click()}>
{file || 'Clique para escolher um PDF (ou arraste aqui)'}
até 50 MB · PDF, PDF/A
e.target.files[0] && setFile(e.target.files[0].name)} />
ORIGEM
{[ { id: 'email', l: 'E-MAIL DIRETO' }, { id: 'portal', l: 'PORTAL EXTERNO' }, { id: 'fisico', l: 'CÓPIA FÍSICA' }, ].map(o => (
setOrigem(o.id)}>{o.l}
))}
OBSERVAÇÃO (opcional)