/* global supa */ // SGL · Camada de acesso a dados (Supabase) // Cada função retorna uma Promise. Erros sobem para o caller. let _orgIdCache = null; async function getCurrentOrgId() { if (_orgIdCache) return _orgIdCache; const { data, error } = await window.supa .from('memberships') .select('organization_id') .limit(1) .single(); if (error) throw error; _orgIdCache = data?.organization_id; return _orgIdCache; } function _normalizeEdital(row) { if (!row) return row; return { ...row, valor: parseFloat(row.valor) || 0 }; } async function listEditais() { const { data, error } = await window.supa .from('editais') .select('*') .order('abertura', { ascending: true }); if (error) throw error; return (data || []).map(_normalizeEdital); } async function insertEdital(fields) { const orgId = await getCurrentOrgId(); const { data: userData } = await window.supa.auth.getUser(); const payload = { organization_id: orgId, numero: fields.numero, orgao: fields.orgao, uf: fields.uf, cidade: fields.cidade || null, plataforma: fields.plataforma, valor: Number(fields.valor) || 0, abertura: fields.abertura, itens: parseInt(fields.itens, 10) || 0, score: fields.score ?? 75, status: fields.status || 'novo', match: fields.match || 'media', verba: fields.verba || null, created_by: userData?.user?.id || null, }; const { data, error } = await window.supa .from('editais') .insert(payload) .select() .single(); if (error) throw error; return _normalizeEdital(data); } async function updateEditalStatus(id, status) { const { data, error } = await window.supa .from('editais') .update({ status }) .eq('id', id) .select() .single(); if (error) throw error; return _normalizeEdital(data); } // ─── EMAILS (M08) ───────────────────────────────────────── async function listEmails() { const { data, error } = await window.supa .from('emails') .select('*') .order('recebido_em', { ascending: false, nullsFirst: false }) .limit(200); if (error) throw error; return data || []; } async function updateEmailStatus(id, status) { const { data, error } = await window.supa .from('emails') .update({ status }) .eq('id', id) .select() .single(); if (error) throw error; return data; } async function updateEditalAnalise(id, analise) { const { data, error } = await window.supa .from('editais') .update({ analise_ia: analise }) .eq('id', id) .select() .single(); if (error) throw error; return _normalizeEdital(data); } // ─── FORNECEDORES ───────────────────────────────────────── async function listFornecedores() { const { data, error } = await window.supa .from('fornecedores') .select('*') .order('nome', { ascending: true }); if (error) throw error; return data || []; } // ─── PRODUTOS ───────────────────────────────────────────── function _normalizeProduto(row) { if (!row) return row; return { id: row.id, sku: row.ean || row.cod, cod: row.cod, ean: row.ean, desc: row.descricao, cat: row.categoria || '—', ncm: row.ncm || '—', st: row.estoque ?? 0, min: row.estoque_minimo ?? 0, max: row.estoque_maximo || Math.max((row.estoque_minimo ?? 0) * 4, 100), custo: parseFloat(row.custo) || 0, preco: parseFloat(row.preco_venda) || 0, fornecedor: row.fornecedor?.nome || '—', fornecedor_id: row.fornecedor_principal_id, inner: row.inner_minimo ?? 1, mov30: { entr: 0, said: 0 }, giro: 0, _raw: row, }; } async function listProdutos() { const { data, error } = await window.supa .from('produtos') .select('*, fornecedor:fornecedores!fornecedor_principal_id(id, nome)') .eq('ativo', true) .order('descricao', { ascending: true }); if (error) throw error; return (data || []).map(_normalizeProduto); } async function listMovimentosByProduto(produtoId, limit = 30) { const { data, error } = await window.supa .from('movimentos_estoque') .select('*') .eq('produto_id', produtoId) .order('ocorrido_em', { ascending: false }) .limit(limit); if (error) throw error; return data || []; } async function insertFornecedor(fields) { const orgId = await getCurrentOrgId(); const { data: userData } = await window.supa.auth.getUser(); const payload = { organization_id: orgId, nome: fields.nome.trim(), cnpj: fields.cnpj?.trim() || null, email: fields.email?.trim() || null, telefone: fields.telefone?.trim() || null, observacoes: fields.observacoes?.trim() || null, ativo: fields.ativo !== false, created_by: userData?.user?.id || null, }; const { data, error } = await window.supa .from('fornecedores') .insert(payload) .select() .single(); if (error) throw error; return data; } async function updateFornecedor(id, fields) { const payload = { nome: fields.nome?.trim(), cnpj: fields.cnpj?.trim() || null, email: fields.email?.trim() || null, telefone: fields.telefone?.trim() || null, observacoes: fields.observacoes?.trim() || null, ativo: fields.ativo !== false, }; const { data, error } = await window.supa .from('fornecedores') .update(payload) .eq('id', id) .select() .single(); if (error) throw error; return data; } async function deleteFornecedor(id) { const { error } = await window.supa .from('fornecedores') .delete() .eq('id', id); if (error) throw error; return true; } async function insertProduto(fields) { const orgId = await getCurrentOrgId(); const { data: userData } = await window.supa.auth.getUser(); const payload = { organization_id: orgId, cod: fields.cod.trim(), ean: fields.ean?.trim() || null, descricao: fields.descricao.trim(), categoria: fields.categoria?.trim() || null, ncm: fields.ncm?.trim() || null, custo: Number(fields.custo) || 0, preco_venda: fields.preco_venda ? Number(fields.preco_venda) : null, inner_minimo: parseInt(fields.inner_minimo, 10) || 1, estoque: parseInt(fields.estoque, 10) || 0, estoque_minimo: parseInt(fields.estoque_minimo, 10) || 0, estoque_maximo: fields.estoque_maximo ? parseInt(fields.estoque_maximo, 10) : null, fornecedor_principal_id: fields.fornecedor_principal_id || null, ativo: fields.ativo !== false, created_by: userData?.user?.id || null, }; const { data, error } = await window.supa .from('produtos') .insert(payload) .select('*, fornecedor:fornecedores!fornecedor_principal_id(id, nome)') .single(); if (error) throw error; return _normalizeProduto(data); } async function updateProduto(id, fields) { const payload = { cod: fields.cod?.trim(), ean: fields.ean?.trim() || null, descricao: fields.descricao?.trim(), categoria: fields.categoria?.trim() || null, ncm: fields.ncm?.trim() || null, custo: Number(fields.custo) || 0, preco_venda: fields.preco_venda ? Number(fields.preco_venda) : null, inner_minimo: parseInt(fields.inner_minimo, 10) || 1, estoque_minimo: parseInt(fields.estoque_minimo, 10) || 0, estoque_maximo: fields.estoque_maximo ? parseInt(fields.estoque_maximo, 10) : null, fornecedor_principal_id: fields.fornecedor_principal_id || null, ativo: fields.ativo !== false, }; // OBS: estoque NÃO é atualizado aqui — apenas via movimentos_estoque (trigger) const { data, error } = await window.supa .from('produtos') .update(payload) .eq('id', id) .select('*, fornecedor:fornecedores!fornecedor_principal_id(id, nome)') .single(); if (error) throw error; return _normalizeProduto(data); } async function deleteProduto(id) { const { error } = await window.supa .from('produtos') .delete() .eq('id', id); if (error) throw error; return true; } async function updateProdutoPreco(id, preco) { const { data, error } = await window.supa .from('produtos') .update({ preco_venda: preco }) .eq('id', id) .select('*, fornecedor:fornecedores!fornecedor_principal_id(id, nome)') .single(); if (error) throw error; return _normalizeProduto(data); } async function insertAjusteEstoque(produtoId, quantidadeReal, observacoes) { const orgId = await getCurrentOrgId(); const { data: userData } = await window.supa.auth.getUser(); const payload = { organization_id: orgId, produto_id: produtoId, tipo: 'ajuste', quantidade: parseInt(quantidadeReal, 10), documento: 'Ajuste de inventário', observacoes: observacoes || null, usuario_id: userData?.user?.id || null, }; const { data, error } = await window.supa .from('movimentos_estoque') .insert(payload) .select() .single(); if (error) throw error; return data; } async function listFornecedoresByProduto(produtoId) { const { data, error } = await window.supa .from('produto_fornecedores') .select('*, fornecedor:fornecedores!fornecedor_id(id, nome, cnpj)') .eq('produto_id', produtoId); if (error) throw error; return data || []; } // ─── EDITAL · ITENS / DOCS / MENSAGENS / HISTÓRICO ───────── async function listEditalItens(editalId) { const { data, error } = await window.supa .from('edital_itens') .select('*, produto:produtos!produto_id(id, cod, descricao, estoque, custo)') .eq('edital_id', editalId) .order('numero_item', { ascending: true }); if (error) throw error; return (data || []).map(row => { const unit = parseFloat(row.valor_unitario_estimado) || 0; const custo = parseFloat(row.produto?.custo) || 0; const margem = unit > 0 ? +((unit - custo) / unit * 100).toFixed(1) : 0; return { id: row.id, numero_item: row.numero_item, desc: row.descricao, btm: row.produto?.cod || null, ncm: row.ncm || '—', qtd: row.quantidade, unit, match: row.match_score ?? 0, margem, estoque: row.produto?.estoque ?? 0, _raw: row, }; }); } async function listEditalDocumentos(editalId) { const { data, error } = await window.supa .from('edital_documentos') .select('*') .eq('edital_id', editalId) .order('created_at', { ascending: true }); if (error) throw error; return (data || []).map(d => ({ id: d.id, nome: d.nome, ext: d.tipo, tamanho: d.tamanho || '—', paginas: d.paginas, publicado: d.publicado_em ? new Date(d.publicado_em).toLocaleDateString('pt-BR') : '—', url: d.url, _raw: d, })); } async function listEditalMensagens(editalId) { const { data, error } = await window.supa .from('edital_mensagens') .select('*') .eq('edital_id', editalId) .order('ocorrido_em', { ascending: false }); if (error) throw error; return (data || []).map(m => ({ id: m.id, de: m.remetente, tipo: m.tipo, // 'oficial' | 'esclarecimento' | 'interna' quando: new Date(m.ocorrido_em).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }), assunto: m.assunto, corpo: m.corpo, _raw: m, })); } async function listEditalHistorico(editalId) { const { data, error } = await window.supa .from('edital_historico') .select('*') .eq('edital_id', editalId) .order('ocorrido_em', { ascending: false }); if (error) throw error; return (data || []).map(h => ({ id: h.id, titulo: h.titulo, quando: new Date(h.ocorrido_em).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }), descricao: h.descricao, tone: h.tom || '', _raw: h, })); } async function insertEditalMensagem(editalId, fields) { const { data: userData } = await window.supa.auth.getUser(); const payload = { edital_id: editalId, remetente: fields.remetente?.trim() || 'Mariana (BTM)', tipo: fields.tipo || 'interna', assunto: fields.assunto.trim(), corpo: fields.corpo.trim(), ocorrido_em: new Date().toISOString(), created_by: userData?.user?.id || null, }; const { data, error } = await window.supa .from('edital_mensagens') .insert(payload) .select() .single(); if (error) throw error; return data; } // ─── PRECIFICAÇÕES ──────────────────────────────────────── function _normalizePrecif(row) { if (!row) return row; return { id: row.id, edital_id: row.edital_id, status: row.status, regime: row.regime_tributario, markup_pct: parseFloat(row.markup_pct) || 60, margem_minima_pct: parseFloat(row.margem_minima_pct) || 35, icms_inter_pct: parseFloat(row.icms_inter_pct) || 0, icms_intra_pct: parseFloat(row.icms_intra_pct) || 0, ipi_pct: parseFloat(row.ipi_pct) || 0, pis_pct: parseFloat(row.pis_pct) || 0, cofins_pct: parseFloat(row.cofins_pct) || 0, proposta_gerada: !!row.proposta_gerada, proposta_versao: row.proposta_versao || 0, edital: row.edital ? { ...row.edital, valor: parseFloat(row.edital.valor) || 0 } : null, _raw: row, }; } async function listPrecificacoes() { const { data, error } = await window.supa .from('precificacoes') .select('*, edital:editais(*)') .order('created_at', { ascending: false }); if (error) throw error; return (data || []).map(_normalizePrecif); } async function listPrecificacaoItens(precificacaoId) { const { data, error } = await window.supa .from('precificacao_itens') .select('*, edital_item:edital_itens(*, produto:produtos!produto_id(id, cod, descricao, custo, inner_minimo, estoque))') .eq('precificacao_id', precificacaoId); if (error) throw error; return (data || []) .map(row => { const ei = row.edital_item || {}; const p = ei.produto || {}; return { id: row.id, precificacao_id: row.precificacao_id, edital_item_id: row.edital_item_id, idx: ei.numero_item || 0, desc: ei.descricao || '', cod: p.cod || null, ncm: ei.ncm || '—', qtd: ei.quantidade || 0, edital_unit: parseFloat(ei.valor_unitario_estimado) || 0, custo: parseFloat(p.custo) || 0, inner: p.inner_minimo || 1, estoque: p.estoque || 0, preco_lance: parseFloat(row.preco_lance) || 0, margem: parseFloat(row.margem_calculada) || 0, frete: parseFloat(row.frete_unitario) || 0, alerta_inner: !!row.alerta_inner, descartado: !!row.descartado, match: ei.match_score ?? 0, _raw: row, }; }) .sort((a, b) => a.idx - b.idx); } async function updatePrecificacao(id, fields) { const payload = {}; if ('status' in fields) payload.status = fields.status; if ('regime' in fields) payload.regime_tributario = fields.regime; if ('markup_pct' in fields) payload.markup_pct = Number(fields.markup_pct); if ('margem_minima_pct' in fields) payload.margem_minima_pct = Number(fields.margem_minima_pct); if ('icms_inter_pct' in fields) payload.icms_inter_pct = Number(fields.icms_inter_pct); if ('icms_intra_pct' in fields) payload.icms_intra_pct = Number(fields.icms_intra_pct); if ('ipi_pct' in fields) payload.ipi_pct = Number(fields.ipi_pct); if ('pis_pct' in fields) payload.pis_pct = Number(fields.pis_pct); if ('cofins_pct' in fields) payload.cofins_pct = Number(fields.cofins_pct); if ('proposta_gerada' in fields) payload.proposta_gerada = !!fields.proposta_gerada; if ('proposta_versao' in fields) payload.proposta_versao = parseInt(fields.proposta_versao, 10); const { data, error } = await window.supa .from('precificacoes') .update(payload) .eq('id', id) .select('*, edital:editais(*)') .single(); if (error) throw error; return _normalizePrecif(data); } // ─── DISPUTAS / LANCES ─────────────────────────────────── async function listDisputas() { const { data, error } = await window.supa .from('disputas') .select('*, edital:editais(*)') .order('created_at', { ascending: false }); if (error) throw error; return (data || []).map(d => ({ ...d, edital: d.edital ? { ...d.edital, valor: parseFloat(d.edital.valor) || 0 } : null, })); } async function insertDisputa({ edital_id, status = 'aguardando' }) { const orgId = await getCurrentOrgId(); const payload = { organization_id: orgId, edital_id, status, }; const { data, error } = await window.supa .from('disputas') .insert(payload) .select('*, edital:editais(*)') .single(); if (error) throw error; return { ...data, edital: data.edital ? { ...data.edital, valor: parseFloat(data.edital.valor) || 0 } : null, }; } async function listLancesByDisputa(disputaId) { const { data, error } = await window.supa .from('disputa_lances') .select('*') .eq('disputa_id', disputaId) .order('ocorrido_em', { ascending: false }); if (error) throw error; return (data || []).map(l => ({ ...l, valor: parseFloat(l.valor) || 0 })); } async function insertLance({ disputa_id, edital_item_id, valor, autor, meu_lance, cnpj_autor }) { const { data: userData } = await window.supa.auth.getUser(); const payload = { disputa_id, edital_item_id, valor: Number(valor), autor: autor || 'BTM', meu_lance: meu_lance !== false, cnpj_autor: cnpj_autor || null, ocorrido_em: new Date().toISOString(), created_by: userData?.user?.id || null, }; const { data, error } = await window.supa .from('disputa_lances') .insert(payload) .select() .single(); if (error) throw error; return { ...data, valor: parseFloat(data.valor) || 0 }; } async function updateDisputaStatus(disputaId, status) { const updates = { status }; if (status === 'live') updates.aberta_em = new Date().toISOString(); if (status === 'encerrada') updates.encerrada_em = new Date().toISOString(); const { data, error } = await window.supa .from('disputas') .update(updates) .eq('id', disputaId) .select() .single(); if (error) throw error; return data; } // ─── EMPENHOS (M05) ─────────────────────────────────────── function _normalizeEmpenho(row) { if (!row) return row; return { id: row.id, edital_id: row.edital_id, numero: row.numero, orgao: row.orgao, orgaoFull: row.orgao_full, uf: row.uf, email: row.email_origem, arp: row.arp, valor: parseFloat(row.valor) || 0, status: row.status, recebido_em: row.recebido_em, dias: row.recebido_em ? Math.floor((Date.now() - new Date(row.recebido_em).getTime()) / 86400000) : 0, prazo: row.prazo_dias || 30, triangulacao: !!row.triangulacao, atraso: !!row.atraso, nf: row.nf_numero || '—', rastreio: row.rastreio, edital: row.edital ? row.edital.numero : null, _raw: row, }; } async function listEmpenhos() { const { data, error } = await window.supa .from('empenhos') .select('*, edital:editais(id, numero)') .order('recebido_em', { ascending: false }); if (error) throw error; return (data || []).map(_normalizeEmpenho); } async function insertEmpenho(fields) { const orgId = await getCurrentOrgId(); const { data, error } = await window.supa .from('empenhos') .insert({ organization_id: orgId, edital_id: fields.edital_id || null, numero: fields.numero, orgao: fields.orgao, orgao_full: fields.orgaoFull || fields.orgao, uf: fields.uf, email_origem: fields.email || null, arp: fields.arp || null, valor: Number(fields.valor) || 0, status: 'rec', triangulacao: !!fields.triangulacao, }) .select('*, edital:editais(id, numero)') .single(); if (error) throw error; return _normalizeEmpenho(data); } async function updateEmpenhoStatus(id, status) { const { data, error } = await window.supa .from('empenhos') .update({ status }) .eq('id', id) .select('*, edital:editais(id, numero)') .single(); if (error) throw error; return _normalizeEmpenho(data); } // ─── POSTAGENS (M06) ────────────────────────────────────── function _normalizePostagem(row) { if (!row) return row; return { id: row.id, rastreio: row.rastreio, empenho_id: row.empenho_id, empenho: row.empenho?.numero || null, modal: row.modal || '—', dest: row.destino_cidade_uf || '—', destFull: row.destino_endereco || row.destino_cidade_uf || '—', postado: row.postado_em ? new Date(row.postado_em).toLocaleDateString('pt-BR') : '—', status: row.status, statusLabel: row.status_label || row.status, sCls: row.status === 'entregue' ? 'lime' : row.status === 'reverso' ? 'amber' : 'cyan', peso: parseFloat(row.peso_kg) || 0, volumes: row.volumes || 0, valor: parseFloat(row.valor_nota) || 0, frete: parseFloat(row.frete) || 0, prazo: row.prazo_estimado ? new Date(row.prazo_estimado).toLocaleDateString('pt-BR') : '—', reverso: !!row.reverso, motivoReverso: row.motivo_reverso, eventos: Array.isArray(row.eventos) ? row.eventos : [], _raw: row, }; } async function listPostagens() { const { data, error } = await window.supa .from('postagens') .select('*, empenho:empenhos!empenho_id(id, numero)') .order('postado_em', { ascending: false, nullsFirst: false }); if (error) throw error; return (data || []).map(_normalizePostagem); } // ─── TRIANGULAÇÕES (M06) ────────────────────────────────── async function listTriangulacoes() { const { data, error } = await window.supa .from('triangulacoes') .select('*, fornecedor:fornecedores(id, nome), empenho:empenhos(id, numero, orgao, uf), itens:triangulacao_itens(*)') .order('emitido_em', { ascending: false }); if (error) throw error; return (data || []).map(t => ({ id: t.numero_pc || t.id, realId: t.id, forn: t.fornecedor?.nome || '—', forn_id: t.fornecedor_id, dest: t.empenho ? `${t.empenho.orgao}/${t.empenho.uf || '?'}` : '—', destEmpenho: t.empenho?.numero || null, val: parseFloat(t.valor_total) || 0, inner: !!t.inner_alerta, sobra: t.sobra_estimada || 0, status: t.status, pedidoCompra: t.numero_pc, emitido: t.emitido_em ? new Date(t.emitido_em).toLocaleDateString('pt-BR') : '—', previsao: t.previsao ? new Date(t.previsao).toLocaleDateString('pt-BR') : '—', itens: (t.itens || []).map(it => ({ desc: it.descricao, cod: null, qtd: it.quantidade, inner: it.inner_minimo, qtdComprar: it.qtd_a_comprar, sobra: it.sobra, custo: parseFloat(it.custo_unitario) || 0, })), _raw: t, })); } // ─── TÍTULOS (M07 — CAR + CAP) ──────────────────────────── function _normalizeTitulo(row) { if (!row) return row; return { id: row.numero, realId: row.id, tipo: row.tipo, vcto: row.vencimento ? new Date(row.vencimento).toLocaleDateString('pt-BR') : '—', emissao: row.emitido_em ? new Date(row.emitido_em).toLocaleDateString('pt-BR') : '—', desc: row.descricao, cliente: row.tipo === 'car' ? row.contraparte_nome : null, fornecedor: row.tipo === 'cap' ? row.contraparte_nome : null, cnpj: row.contraparte_cnpj, valor: parseFloat(row.valor) || 0, status: row.status, empenho: row.empenho?.numero || null, nf: row.nf_numero, nfOrigem: row.tipo === 'cap' ? row.nf_numero : null, forma: row.forma_pagamento, banco: row.banco, pagoEm: row.pago_em ? new Date(row.pago_em).toLocaleDateString('pt-BR') : null, _raw: row, }; } async function listTitulos(tipo) { let query = window.supa .from('titulos') .select('*, empenho:empenhos!empenho_id(id, numero)') .order('vencimento', { ascending: true }); if (tipo) query = query.eq('tipo', tipo); const { data, error } = await query; if (error) throw error; return (data || []).map(_normalizeTitulo); } async function insertTitulo(fields) { const orgId = await getCurrentOrgId(); const { data, error } = await window.supa .from('titulos') .insert({ organization_id: orgId, numero: fields.numero, tipo: fields.tipo, descricao: fields.descricao || null, valor: Number(fields.valor) || 0, vencimento: fields.vencimento, emitido_em: fields.emitido_em || null, status: fields.status || 'aberto', forma_pagamento: fields.forma_pagamento || null, banco: fields.banco || null, contraparte_nome: fields.contraparte_nome, contraparte_cnpj: fields.contraparte_cnpj || null, empenho_id: fields.empenho_id || null, fornecedor_id: fields.fornecedor_id || null, nf_numero: fields.nf_numero || null, }) .select('*, empenho:empenhos!empenho_id(id, numero)') .single(); if (error) throw error; return _normalizeTitulo(data); } async function updateTituloStatus(id, status) { const updates = { status }; if (status === 'pago') updates.pago_em = new Date().toISOString().slice(0, 10); const { data, error } = await window.supa .from('titulos') .update(updates) .eq('id', id) .select('*, empenho:empenhos!empenho_id(id, numero)') .single(); if (error) throw error; return _normalizeTitulo(data); } // ─── NFS CONTRA BTM (M07) ───────────────────────────────── function _normalizeNfContra(row) { if (!row) return row; return { id: row.id, nf: row.numero_nf, emitente: row.emitente_nome, cnpj: row.emitente_cnpj, uf: row.uf, emissao: row.emissao ? new Date(row.emissao).toLocaleDateString('pt-BR') : '—', valor: parseFloat(row.valor_total) || 0, venc: row.vencimento ? new Date(row.vencimento).toLocaleDateString('pt-BR') : '—', status: row.status, cfop: row.cfop || '—', baseICMS: parseFloat(row.base_icms) || 0, icms: parseFloat(row.icms) || 0, ipi: parseFloat(row.ipi) || 0, frete: parseFloat(row.frete) || 0, observacao: row.observacoes, pagoEm: row.pago_em ? new Date(row.pago_em).toLocaleDateString('pt-BR') : null, _raw: row, }; } async function listNfsContra() { const { data, error } = await window.supa .from('nfs_contra') .select('*') .order('emissao', { ascending: false }); if (error) throw error; return (data || []).map(_normalizeNfContra); } async function updateNfContraStatus(id, status) { const updates = { status }; if (status === 'pago') updates.pago_em = new Date().toISOString().slice(0, 10); const { data, error } = await window.supa .from('nfs_contra') .update(updates) .eq('id', id) .select() .single(); if (error) throw error; return _normalizeNfContra(data); } // ─── CERTIDÕES (Fase 6) ─────────────────────────────────── function _normalizeCertidao(row) { if (!row) return row; const validade = row.validade ? new Date(row.validade) : null; const today = new Date(); today.setHours(0, 0, 0, 0); const dias = validade ? Math.floor((validade - today) / 86400000) : 999; let status; if (dias < 0) status = 'vencida'; else if (dias <= 7) status = 'critico'; else if (dias <= 30) status = 'alerta'; else status = 'vigente'; return { id: row.id, nome: row.nome, orgao: row.orgao, numero: row.numero, emissao: row.emissao, validade: row.validade, dias, status, arquivo_url: row.arquivo_url, observacoes: row.observacoes, ativo: row.ativo, _raw: row, }; } async function listCertidoes() { const { data, error } = await window.supa .from('certidoes') .select('*') .eq('ativo', true) .order('validade', { ascending: true }); if (error) throw error; return (data || []).map(_normalizeCertidao); } // ─── AUDITORIA LOG (Fase 6) ─────────────────────────────── async function listAuditoriaEvents({ tipo, modulo, limit = 100 } = {}) { let q = window.supa .from('auditoria_log') .select('*') .order('ts', { ascending: false }) .limit(limit); if (tipo) q = q.eq('tipo', tipo); if (modulo) q = q.eq('modulo', modulo); const { data, error } = await q; if (error) throw error; return data || []; } async function logAuditEvent({ evento, modulo, tipo, detalhes, valor_relacionado, modulo_target, autor }) { const orgId = await getCurrentOrgId(); const { data: userData } = await window.supa.auth.getUser(); const payload = { organization_id: orgId, autor: autor || userData?.user?.email?.split('@')[0] || 'Sistema', autor_user_id: userData?.user?.id || null, evento, modulo: modulo || null, tipo: tipo || 'audit', detalhes: detalhes || null, valor_relacionado: valor_relacionado != null ? Number(valor_relacionado) : null, modulo_target: modulo_target || null, }; const { data, error } = await window.supa .from('auditoria_log') .insert(payload) .select() .single(); if (error) throw error; return data; } // ─── CLIENTES (M11) ─────────────────────────────────────── async function listClientes() { const { data, error } = await window.supa .from('clientes') .select('*') .order('nome', { ascending: true }); if (error) throw error; return data || []; } async function insertCliente(fields) { const orgId = await getCurrentOrgId(); const { data: userData } = await window.supa.auth.getUser(); const { data, error } = await window.supa .from('clientes') .insert({ organization_id: orgId, nome: fields.nome.trim(), nome_fantasia: fields.nome_fantasia?.trim() || null, cnpj: fields.cnpj?.trim() || null, tipo: fields.tipo || 'publico', email: fields.email?.trim() || null, telefone: fields.telefone?.trim() || null, endereco: fields.endereco?.trim() || null, cidade: fields.cidade?.trim() || null, uf: fields.uf?.trim() || null, cep: fields.cep?.trim() || null, contato_responsavel: fields.contato_responsavel?.trim() || null, observacoes: fields.observacoes?.trim() || null, ativo: fields.ativo !== false, created_by: userData?.user?.id || null, }) .select() .single(); if (error) throw error; return data; } async function updateCliente(id, fields) { const payload = { nome: fields.nome?.trim(), nome_fantasia: fields.nome_fantasia?.trim() || null, cnpj: fields.cnpj?.trim() || null, tipo: fields.tipo || 'publico', email: fields.email?.trim() || null, telefone: fields.telefone?.trim() || null, endereco: fields.endereco?.trim() || null, cidade: fields.cidade?.trim() || null, uf: fields.uf?.trim() || null, cep: fields.cep?.trim() || null, contato_responsavel: fields.contato_responsavel?.trim() || null, observacoes: fields.observacoes?.trim() || null, ativo: fields.ativo !== false, }; const { data, error } = await window.supa .from('clientes') .update(payload) .eq('id', id) .select() .single(); if (error) throw error; return data; } async function deleteCliente(id) { const { error } = await window.supa.from('clientes').delete().eq('id', id); if (error) throw error; return true; } async function listClienteRelations(cliente) { const tokens = [cliente.nome_fantasia, cliente.nome].filter(Boolean); const orgaoFilter = tokens.map(t => `orgao.ilike.%${t}%`).join(','); let titulosQuery = window.supa .from('titulos') .select('*, empenho:empenhos!empenho_id(id, numero)') .eq('tipo', 'car'); if (cliente.cnpj) { titulosQuery = titulosQuery.eq('contraparte_cnpj', cliente.cnpj); } else if (tokens.length) { titulosQuery = titulosQuery.or(tokens.map(t => `contraparte_nome.ilike.%${t}%`).join(',')); } titulosQuery = titulosQuery.order('vencimento', { ascending: true }).limit(50); const [edRes, empRes, titRes] = await Promise.all([ tokens.length ? window.supa.from('editais') .select('id, numero, orgao, uf, valor, status, abertura') .or(orgaoFilter) .order('abertura', { ascending: false }).limit(50) : Promise.resolve({ data: [] }), tokens.length ? window.supa.from('empenhos') .select('id, numero, orgao, uf, valor, status, recebido_em, nf_numero') .or(orgaoFilter) .order('recebido_em', { ascending: false }).limit(50) : Promise.resolve({ data: [] }), titulosQuery, ]); return { editais: (edRes.data || []).map(e => ({ ...e, valor: parseFloat(e.valor) || 0 })), empenhos: (empRes.data || []).map(e => ({ ...e, valor: parseFloat(e.valor) || 0 })), titulos: (titRes.data || []).map(_normalizeTitulo), }; } async function getDashboardStats() { const [editais, produtos, fornecedores] = await Promise.all([ listEditais(), listProdutos(), listFornecedores(), ]); return { editais, produtos, fornecedores }; } async function getTickerStats() { const orgId = await getCurrentOrgId(); const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); const [editaisRes, empenhosRes, titulosRes, certRes, lancesRes] = await Promise.all([ window.supa.from('editais').select('status', { count: 'exact' }).eq('organization_id', orgId), window.supa.from('empenhos').select('status', { count: 'exact' }).eq('organization_id', orgId), window.supa.from('titulos').select('valor, status, tipo').eq('organization_id', orgId).eq('tipo', 'car').neq('status', 'pago'), window.supa.from('certidoes').select('vencimento').eq('organization_id', orgId).order('vencimento', { ascending: true }).limit(1), window.supa.from('disputa_lances').select('id', { count: 'exact', head: true }).gte('ocorrido_em', todayStart.toISOString()), ]); const editaisAtivos = (editaisRes.data || []).filter(e => !['descartado', 'ganho'].includes(e.status)).length; const empenhosAtivos = (empenhosRes.data || []).filter(e => e.status !== 'pago').length; const aReceber = (titulosRes.data || []).reduce((s, t) => s + (parseFloat(t.valor) || 0), 0); const cndDias = certRes.data?.[0]?.vencimento ? Math.max(0, Math.ceil((new Date(certRes.data[0].vencimento) - Date.now()) / 86400000)) : null; const lancesHoje = lancesRes.count || 0; return { editaisAtivos, empenhosAtivos, aReceber, cndDias, lancesHoje }; } window.dataApi = { getCurrentOrgId, getDashboardStats, getTickerStats, listEditais, insertEdital, updateEditalStatus, updateEditalAnalise, listEmails, updateEmailStatus, listFornecedores, insertFornecedor, updateFornecedor, deleteFornecedor, listProdutos, insertProduto, updateProduto, deleteProduto, updateProdutoPreco, insertAjusteEstoque, listMovimentosByProduto, listFornecedoresByProduto, listEditalItens, listEditalDocumentos, listEditalMensagens, listEditalHistorico, insertEditalMensagem, listPrecificacoes, listPrecificacaoItens, updatePrecificacao, listDisputas, insertDisputa, listLancesByDisputa, insertLance, updateDisputaStatus, listEmpenhos, insertEmpenho, updateEmpenhoStatus, listPostagens, listTriangulacoes, listTitulos, insertTitulo, updateTituloStatus, listNfsContra, updateNfContraStatus, listCertidoes, listAuditoriaEvents, logAuditEvent, listClientes, insertCliente, updateCliente, deleteCliente, listClienteRelations, };