portal_publico/static/js/conciliacao-fornecedores.js

1039 lines
50 KiB
JavaScript

/**
* Controlador de "Conciliação de Fornecedores" (Utilitários), ver CLAUDE.md
* do pacote `portal_api/conciliacao_fornecedores/`. Guarda de página no
* mesmo padrão de `nao-conformidades.js`: retorna cedo se o elemento raiz da
* tela não existir e some atrás de `#conc-no-access` sem
* `permissoes_efetivas.utilitarios.apps["conciliacao-fornecedores"]`.
*
* Duas vistas na mesma página: histórico (+ formulário de nova conciliação)
* e detalhe de uma conciliação (cards de alerta + tabela de fornecedores
* expansível). O detalhe abre por `#conciliacao-<id>` na URL, para um
* recarregamento voltar à mesma conciliação.
*/
const PID_CONC_MOEDA = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" });
const PID_CONC_STATUS = {
divergente: { rotulo: "Divergente", pill: "status-pill--danger", rank: 0 },
atencao: { rotulo: "Atenção", pill: "status-pill--warning", rank: 1 },
pendente: { rotulo: "Pendente de pagamento", pill: "status-pill--neutral", rank: 2 },
conciliada: { rotulo: "Conciliada", pill: "status-pill--ok", rank: 3 },
};
const PID_CONC_ROTULO_ALERTA = {
credito_vencido: "Títulos em aberto há mais de 3 meses",
debito_excedente: "Pagamentos sem título em aberto",
pagamento_sem_titulo: "Pagamentos sem título de mesmo valor",
diferenca_vinculo: "Diferenças de juros/desconto",
conta_transitoria: "Conta transitória com saldo",
saldo_devedor: "Saldo devedor",
saldo_divergente_arquivo: "Saldo diferente do arquivo",
};
const PID_CONC_SEVERIDADE = { alta: "Alta", media: "Média", baixa: "Baixa" };
function pidConcEscape(texto) {
if (texto == null) return "";
const div = document.createElement("div");
div.textContent = String(texto);
return div.innerHTML;
}
function pidConcMoeda(valor) {
return PID_CONC_MOEDA.format(Number(valor) || 0);
}
/** Saldo com o lado ("C" credor positivo, "D" devedor negativo), como o Questor imprime. */
function pidConcSaldo(valor) {
const numero = Number(valor) || 0;
if (numero === 0) return pidConcMoeda(0);
return `${pidConcMoeda(Math.abs(numero))} ${numero > 0 ? "C" : "D"}`;
}
function pidConcData(iso) {
if (!iso) return "—";
const [ano, mes, dia] = iso.slice(0, 10).split("-");
return `${dia}/${mes}/${ano}`;
}
function pidConcDataHora(iso) {
if (!iso) return "—";
const data = new Date(iso);
if (Number.isNaN(data.getTime())) return "—";
return data.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
}
function pidConcCompara(a, b) {
if (typeof a === "number" && typeof b === "number") return a - b;
return String(a ?? "").localeCompare(String(b ?? ""), "pt-BR", { sensitivity: "base", numeric: true });
}
/** Valor em aberto de um item: sempre o real (só vínculo muda valor). */
function pidConcValorAbertoHtml(situacao) {
if (!situacao || !Number(situacao.valor_em_aberto)) return "—";
return pidConcMoeda(situacao.valor_em_aberto);
}
/** Célula de situação: o fato primeiro; a dedução da ferramenta, se houver,
* numa linha à parte marcada como sugestão (nunca misturada ao fato). */
function pidConcSituacaoHtml(situacao) {
if (!situacao) return '<td class="conc-situacao"></td>';
const sugestao = situacao.sugestao
? `<span class="conc-sugestao"><span class="conc-sugestao-tag">Sugestão</span>${pidConcEscape(situacao.sugestao)}</span>`
: "";
return `<td class="conc-situacao conc-situacao--${situacao.categoria}">${pidConcEscape(situacao.descricao)}${sugestao}</td>`;
}
function pidConcTextoValidacao(conta) {
if (!conta.validada) return "";
if (conta.validada_automatica) return `Validada automaticamente (conta conciliada) em ${pidConcDataHora(conta.validada_em)}`;
return `Validada por ${conta.validada_por_nome || "—"} em ${pidConcDataHora(conta.validada_em)}`;
}
function pidConcStatusHtml(status) {
const info = PID_CONC_STATUS[status] || PID_CONC_STATUS.pendente;
return `<span class="status-pill ${info.pill}">${info.rotulo}</span>`;
}
document.addEventListener("DOMContentLoaded", async () => {
const contentEl = document.getElementById("conc-content");
if (!contentEl) return;
const me = await pidGetMe();
if (!me) return;
const utilitarios = (me.permissoes_efetivas && me.permissoes_efetivas["utilitarios"]) || {};
const podeUsar = !!(utilitarios.apps && utilitarios.apps["conciliacao-fornecedores"]);
document.getElementById("conc-no-access").hidden = podeUsar;
contentEl.hidden = !podeUsar;
if (!podeUsar) return;
const listView = document.getElementById("conc-list-view");
const detailView = document.getElementById("conc-detail-view");
// ======================================================================
// Histórico + nova conciliação
// ======================================================================
const formEl = document.getElementById("conc-form");
const novaBtn = document.getElementById("conc-nova-btn");
const codigoInput = document.getElementById("conc-form-codigo");
const empresaEl = document.getElementById("conc-form-empresa");
const arquivoInput = document.getElementById("conc-form-arquivo");
const arquivoNomeEl = document.getElementById("conc-form-arquivo-nome");
const formErroEl = document.getElementById("conc-form-erro");
const processarBtn = document.getElementById("conc-form-processar-btn");
const listaBody = document.getElementById("conc-lista-body");
const listaVazia = document.getElementById("conc-lista-vazia");
const listaBusca = document.getElementById("conc-lista-busca");
let historico = [];
let nomeEmpresaResolvido = null; // null = não consultado/não encontrado
let timerNomeEmpresa = null;
let consultaNomeAtual = 0;
function limparFormulario() {
codigoInput.value = "";
empresaEl.textContent = "";
empresaEl.className = "conc-form__empresa";
arquivoInput.value = "";
arquivoNomeEl.textContent = "Nenhum arquivo selecionado";
formErroEl.textContent = "";
nomeEmpresaResolvido = null;
}
novaBtn.addEventListener("click", () => {
const abrir = formEl.hidden;
formEl.hidden = !abrir;
if (abrir) {
limparFormulario();
codigoInput.focus();
}
});
document.getElementById("conc-form-cancelar-btn").addEventListener("click", () => {
formEl.hidden = true;
});
arquivoInput.addEventListener("change", () => {
const arquivo = arquivoInput.files[0];
arquivoNomeEl.textContent = arquivo ? arquivo.name : "Nenhum arquivo selecionado";
// O nome do arquivo costuma começar pelo código da empresa ("2017 - ..."):
// sugere, sem sobrescrever o que já foi digitado.
const prefixo = arquivo && arquivo.name.match(/^(\d+)\s*[-_ ]/);
if (prefixo && !codigoInput.value.trim()) {
codigoInput.value = prefixo[1];
buscarNomeEmpresa();
}
});
async function buscarNomeEmpresa() {
const codigo = codigoInput.value.trim();
nomeEmpresaResolvido = null;
if (!codigo) {
empresaEl.textContent = "";
empresaEl.className = "conc-form__empresa";
return;
}
if (!/^\d+$/.test(codigo)) {
empresaEl.textContent = "Informe apenas números.";
empresaEl.className = "conc-form__empresa is-erro";
return;
}
const consulta = ++consultaNomeAtual;
empresaEl.textContent = "Buscando no Questor...";
empresaEl.className = "conc-form__empresa is-carregando";
try {
const dados = await pidApiRequest(`/conciliacoes-fornecedores/nome-empresa/?codigo_empresa=${encodeURIComponent(codigo)}`);
if (consulta !== consultaNomeAtual) return;
nomeEmpresaResolvido = dados.nome_empresa;
empresaEl.textContent = dados.nome_empresa || "Empresa não encontrada no Questor.";
empresaEl.className = `conc-form__empresa${dados.nome_empresa ? "" : " is-erro"}`;
} catch (erro) {
if (consulta !== consultaNomeAtual) return;
empresaEl.textContent = erro.message;
empresaEl.className = "conc-form__empresa is-erro";
}
}
codigoInput.addEventListener("input", () => {
clearTimeout(timerNomeEmpresa);
timerNomeEmpresa = setTimeout(buscarNomeEmpresa, 450);
});
processarBtn.addEventListener("click", async () => {
formErroEl.textContent = "";
const codigo = codigoInput.value.trim();
const arquivo = arquivoInput.files[0];
if (!codigo) {
formErroEl.textContent = "Informe o código da empresa.";
return;
}
if (!arquivo) {
formErroEl.textContent = "Selecione o arquivo do razão.";
return;
}
clearTimeout(timerNomeEmpresa);
if (nomeEmpresaResolvido === null) await buscarNomeEmpresa();
if (!nomeEmpresaResolvido) {
const seguir = await pidConfirm(
`A empresa ${codigo} não foi encontrada no Questor (ou o Questor está inacessível agora). Processar assim mesmo?`,
{ titulo: "Empresa não encontrada", textoConfirmar: "Processar" }
);
if (!seguir) return;
}
const dados = new FormData();
dados.append("codigo_empresa", codigo);
dados.append("arquivo", arquivo);
processarBtn.disabled = true;
processarBtn.textContent = "Processando...";
try {
const criada = await pidApiRequest("/conciliacoes-fornecedores/", { method: "POST", body: dados });
formEl.hidden = true;
await carregarHistorico();
abrirDetalhe(criada.id);
} catch (erro) {
formErroEl.textContent = erro.message;
} finally {
processarBtn.disabled = false;
processarBtn.textContent = "Processar";
}
});
function renderHistorico() {
const termo = listaBusca.value.trim().toLowerCase();
const itens = historico.filter(
(item) => !termo || `${item.codigo_empresa} ${item.nome_empresa}`.toLowerCase().includes(termo)
);
listaBody.innerHTML = "";
listaVazia.hidden = itens.length > 0;
listaVazia.textContent = historico.length ? "Nenhuma conciliação para esta busca." : "Nenhuma conciliação realizada ainda.";
itens.forEach((item) => {
const resumo = item.resumo || {};
const porStatus = resumo.por_status || {};
const conciliados = porStatus.conciliada || 0;
const periodo = item.periodo_inicio ? `${pidConcData(item.periodo_inicio)} a ${pidConcData(item.periodo_fim)}` : "—";
const tr = document.createElement("tr");
tr.className = "conc-row";
tr.innerHTML = `
<td><span class="pa-name-link">${pidConcEscape(item.codigo_empresa)} - ${pidConcEscape(item.nome_empresa || "Empresa não encontrada no Questor")}</span></td>
<td>${pidConcData(item.data_base)}</td>
<td>${periodo}</td>
<td class="conc-num">${resumo.total_contas ?? "—"}</td>
<td class="conc-num">${conciliados}</td>
<td class="conc-num">${porStatus.divergente ? `<span class="conc-valor--devedor">${porStatus.divergente}</span>` : 0}</td>
<td class="conc-num conc-valor">${pidConcMoeda(resumo.total_em_aberto)}</td>
<td>${pidConcEscape(item.criado_por_nome || "—")}<span class="conc-fornecedor__alerta">${pidConcDataHora(item.criado_em)}</span></td>
<td class="conc-num"><button type="button" class="btn-danger-outline conc-btn-sm" data-excluir>Excluir</button></td>`;
tr.addEventListener("click", (evento) => {
if (evento.target.closest("[data-excluir]")) return;
abrirDetalhe(item.id);
});
tr.querySelector("[data-excluir]").addEventListener("click", async () => {
const confirmado = await pidConfirm(
`Excluir a conciliação de ${item.codigo_empresa} - ${item.nome_empresa || ""} (data-base ${pidConcData(item.data_base)})? Os vínculos manuais e validações feitos nela também serão excluídos.`,
{ titulo: "Excluir conciliação", textoConfirmar: "Excluir", perigoso: true }
);
if (!confirmado) return;
try {
await pidApiRequest(`/conciliacoes-fornecedores/${item.id}/`, { method: "DELETE" });
await carregarHistorico();
} catch (erro) {
pidAlert(erro.message, { titulo: "Não foi possível excluir" });
}
});
listaBody.appendChild(tr);
});
}
listaBusca.addEventListener("input", renderHistorico);
async function carregarHistorico() {
try {
historico = await pidApiRequest("/conciliacoes-fornecedores/");
} catch (erro) {
historico = [];
listaVazia.hidden = false;
listaVazia.textContent = erro.message;
return;
}
renderHistorico();
}
// ======================================================================
// Detalhe
// ======================================================================
const cardsEl = document.getElementById("conc-cards");
const contasBody = document.getElementById("conc-contas-body");
const contasVazia = document.getElementById("conc-contas-vazia");
const contasBusca = document.getElementById("conc-contas-busca");
const filtroStatusBtns = Array.from(document.querySelectorAll("[data-conc-status]"));
const filtroAlertaEl = document.getElementById("conc-filtro-alerta");
const sortHeaders = Array.from(document.querySelectorAll("#conc-contas-table th[data-sort]"));
const mostrarValidadasInput = document.getElementById("conc-mostrar-validadas");
const mostrarValidadasQtd = document.getElementById("conc-mostrar-validadas-qtd");
let detalhe = null;
let filtroStatus = "";
let filtroAlerta = "";
// Padrão: por situação, divergentes primeiro (pedido do usuário).
const SORT_PADRAO = "status";
let sortKey = SORT_PADRAO;
let sortDir = 1;
const abertas = new Set();
// estado de cada conta expandida: lançamentos selecionados e se mostra os já vinculados
const estadoConta = new Map();
function estadoDe(contaId) {
if (!estadoConta.has(contaId)) estadoConta.set(contaId, { selecionados: new Set(), mostrarVinculados: false, ocultarConciliados: false });
return estadoConta.get(contaId);
}
document.getElementById("conc-voltar-btn").addEventListener("click", () => {
history.replaceState(null, "", window.location.pathname);
mostrarLista();
});
function mostrarLista() {
detalhe = null;
detailView.hidden = true;
listView.hidden = false;
carregarHistorico();
}
async function abrirDetalhe(id) {
listView.hidden = true;
detailView.hidden = false;
cardsEl.innerHTML = "";
contasBody.innerHTML = '<tr><td colspan="8" class="conc-empty">Carregando...</td></tr>';
history.replaceState(null, "", `#conciliacao-${id}`);
abertas.clear();
estadoConta.clear();
filtroStatus = "";
filtroAlerta = "";
contasBusca.value = "";
mostrarValidadasInput.checked = false;
sortKey = SORT_PADRAO;
sortDir = 1;
atualizarSortHeaders();
try {
detalhe = await pidApiRequest(`/conciliacoes-fornecedores/${id}/`);
} catch (erro) {
contasBody.innerHTML = `<tr><td colspan="8" class="conc-empty">${pidConcEscape(erro.message)}</td></tr>`;
return;
}
renderCabecalho();
renderCards();
renderContas();
}
function renderCabecalho() {
document.getElementById("conc-det-codigo").textContent = `EMPRESA ${detalhe.codigo_empresa}`;
document.getElementById("conc-det-nome").textContent = detalhe.nome_empresa || "Empresa não encontrada no Questor";
const periodo = detalhe.periodo_inicio ? `${pidConcData(detalhe.periodo_inicio)} a ${pidConcData(detalhe.periodo_fim)}` : "sem lançamentos";
document.getElementById("conc-det-meta").innerHTML = `
<span>Data-base <strong>${pidConcData(detalhe.data_base)}</strong></span>
<span>Período do razão <strong>${periodo}</strong></span>
<span>Arquivo <strong>${pidConcEscape(detalhe.nome_arquivo || "—")}</strong></span>
<span>Processado por <strong>${pidConcEscape(detalhe.criado_por_nome || "—")}</strong> em ${pidConcDataHora(detalhe.criado_em)}</span>`;
document.getElementById("conc-exportar-btn").href = `${PID_API_BASE}/conciliacoes-fornecedores/${detalhe.id}/exportar-xlsx/`;
}
function contasComAlerta(tipo) {
return detalhe.contas.filter((c) => c.alertas.some((a) => a.tipo === tipo));
}
/** Faixa de indicadores (3 valores + progresso da validação) e etiquetas
* dos apontamentos menores. Os números por situação ficam só nos chips,
* que já filtram (não duplicar em card). */
function renderCards() {
const contas = detalhe.contas;
const porStatus = {};
contas.forEach((c) => {
porStatus[c.status] = (porStatus[c.status] || 0) + 1;
});
const soma = (lista, campo) => lista.reduce((total, c) => total + (Number(c[campo]) || 0), 0);
const vencidas = contasComAlerta("credito_vencido");
const excedentes = contas.filter((c) => Number(c.total_excedente) > 0);
const validados = contas.filter((c) => c.validada).length;
const pct = contas.length ? Math.round((validados / contas.length) * 100) : 0;
const kpi = (rotulo, valor, detalheTxt, classe, alerta) => {
const tag = alerta ? "button" : "div";
const ativo = alerta && filtroAlerta === alerta ? " is-active" : "";
const atributos = alerta ? ` type="button" data-conc-alerta="${alerta}"` : "";
return `<${tag} class="conc-kpi ${classe}${ativo}"${atributos}>
<span class="conc-kpi__rotulo">${rotulo}</span>
<span class="conc-kpi__valor">${valor}</span>
<span class="conc-kpi__detalhe">${detalheTxt}</span>
</${tag}>`;
};
const apontamentos = [
["diferenca_vinculo", "com diferença de juros/desconto", "info"],
["pagamento_sem_titulo", "com pagamento não vinculado", "warning"],
["conta_transitoria", "conta transitória com saldo", "danger"],
]
.map(([tipo, texto, classe]) => [tipo, contasComAlerta(tipo).length, texto, classe])
.filter(([, quantidade]) => quantidade > 0)
.map(
([tipo, quantidade, texto, classe]) =>
`<button type="button" class="conc-tag conc-tag--${classe}${filtroAlerta === tipo ? " is-active" : ""}" data-conc-alerta="${tipo}"><strong>${quantidade}</strong> ${texto}</button>`
)
.join("");
cardsEl.innerHTML = `
<div class="conc-kpis">
${kpi(
"Títulos em aberto",
pidConcMoeda(soma(contas, "total_em_aberto")),
soma(contas, "total_nao_vinculado")
? `sem pagamento vinculado · ${pidConcMoeda(soma(contas, "total_nao_vinculado"))} em pagamentos a vincular`
: "sem pagamento vinculado",
"",
""
)}
${kpi("Em aberto há mais de 3 meses", pidConcMoeda(soma(vencidas, "total_vencido")), `${vencidas.length} fornecedor(es)`, "conc-kpi--warning", "credito_vencido")}
${kpi("Pagamentos sem título", pidConcMoeda(soma(excedentes, "total_excedente")), `${excedentes.length} fornecedor(es) · divergência`, "conc-kpi--danger", "debito_excedente")}
<div class="conc-progresso">
<div class="conc-progresso__topo"><span class="conc-kpi__rotulo">Validação</span><span><strong>${validados}</strong> de ${contas.length} fornecedores</span></div>
<div class="conc-progresso__barra"><span style="width:${pct}%"></span></div>
<span class="conc-kpi__detalhe">${contas.length - validados} ainda para revisar · ${pct}% concluído</span>
</div>
</div>
${apontamentos ? `<div class="conc-tags"><span class="conc-tags__rotulo">Apontamentos:</span>${apontamentos}</div>` : ""}`;
cardsEl.querySelectorAll("[data-conc-alerta]").forEach((el) => {
el.addEventListener("click", () => {
const alerta = el.dataset.concAlerta;
filtroAlerta = filtroAlerta === alerta ? "" : alerta;
filtroStatus = "";
renderCards();
renderContas();
});
});
filtroStatusBtns.forEach((btn) => {
const status = btn.dataset.concStatus;
const quantidade = status === "validada" ? validados : status ? porStatus[status] || 0 : contas.length;
btn.innerHTML = `${btn.textContent.replace(/\s*\d+$/, "")} <span class="conc-chip__count">${quantidade}</span>`;
btn.classList.toggle("is-active", !filtroAlerta && filtroStatus === status);
});
mostrarValidadasQtd.textContent = validados;
}
filtroStatusBtns.forEach((btn) => {
btn.addEventListener("click", () => {
filtroStatus = btn.dataset.concStatus;
filtroAlerta = "";
renderCards();
renderContas();
});
});
contasBusca.addEventListener("input", renderContas);
mostrarValidadasInput.addEventListener("change", renderContas);
const sortValor = {
conta_numero: (c) => Number(c.conta_numero),
fornecedor: (c) => c.fornecedor,
saldo_inicial: (c) => Number(c.saldo_inicial),
saldo_final: (c) => Number(c.saldo_final),
total_vencido: (c) => Number(c.total_vencido),
status: (c) => PID_CONC_STATUS[c.status].rank,
validada: (c) => (c.validada ? 1 : 0),
};
sortHeaders.forEach((th) => {
th.addEventListener("click", () => {
const chave = th.dataset.sort;
sortDir = sortKey === chave ? sortDir * -1 : 1;
sortKey = chave;
atualizarSortHeaders();
renderContas();
});
});
function atualizarSortHeaders() {
sortHeaders.forEach((h) => {
h.classList.toggle("is-sort-asc", h.dataset.sort === sortKey && sortDir === 1);
h.classList.toggle("is-sort-desc", h.dataset.sort === sortKey && sortDir === -1);
});
}
function contasFiltradas() {
const termo = contasBusca.value.trim().toLowerCase();
let lista = detalhe.contas.filter((c) => {
// Validadas ficam fora por padrão (pedido do usuário): a lista mostra
// o que ainda falta revisar. O chip "Validados" as mostra sempre.
if (c.validada && !mostrarValidadasInput.checked && filtroStatus !== "validada") return false;
if (filtroAlerta) {
const temAlerta =
filtroAlerta === "debito_excedente" ? Number(c.total_excedente) > 0 : c.alertas.some((a) => a.tipo === filtroAlerta);
if (!temAlerta) return false;
} else if (filtroStatus === "validada") {
// validação é uma marcação à parte da situação calculada
if (!c.validada) return false;
} else if (filtroStatus && c.status !== filtroStatus) {
return false;
}
return !termo || `${c.conta_numero} ${c.fornecedor}`.toLowerCase().includes(termo);
});
if (sortKey) {
const extrator = sortValor[sortKey];
lista = lista.slice().sort((a, b) => pidConcCompara(extrator(a), extrator(b)) * sortDir);
}
return lista;
}
function renderContas() {
filtroAlertaEl.hidden = !filtroAlerta;
if (filtroAlerta) {
filtroAlertaEl.innerHTML = `Filtrando por: <strong>${PID_CONC_ROTULO_ALERTA[filtroAlerta]}</strong><button type="button">Limpar filtro</button>`;
filtroAlertaEl.querySelector("button").addEventListener("click", () => {
filtroAlerta = "";
renderCards();
renderContas();
});
}
const lista = contasFiltradas();
contasBody.innerHTML = "";
contasVazia.hidden = lista.length > 0;
contasVazia.textContent =
!mostrarValidadasInput.checked && !filtroAlerta && !filtroStatus && !contasBusca.value.trim()
? "Todos os fornecedores estão validados. Marque \"Mostrar validadas\" para vê-los."
: "Nenhum fornecedor para este filtro.";
const fragmento = document.createDocumentFragment();
lista.forEach((conta) => montarLinhasConta(conta).forEach((tr) => fragmento.appendChild(tr)));
contasBody.appendChild(fragmento);
}
function resumoAlertas(conta) {
if (!conta.alertas.length) return conta.qtd_pendentes ? `${conta.qtd_pendentes} lançamento(s) pendente(s)` : "";
// só o primeiro alerta, cortado com reticências: o resto está na expansão
return conta.alertas[0].mensagem;
}
function montarLinhasConta(conta) {
const aberta = abertas.has(conta.id);
const tr = document.createElement("tr");
tr.className = `conc-row${aberta ? " is-aberta" : ""}`;
tr.dataset.contaId = conta.id;
const saldoFinal = Number(conta.saldo_final);
const vencido = Number(conta.total_vencido);
tr.innerHTML = `
<td><span class="conc-expand-btn"><svg class="conc-expand-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></td>
<td class="conc-conta-numero">${pidConcEscape(conta.conta_numero)}</td>
<td><span class="conc-fornecedor__nome">${pidConcEscape(conta.fornecedor)}</span><span class="conc-fornecedor__alerta">${pidConcEscape(resumoAlertas(conta))}</span></td>
<td class="conc-num conc-valor${Number(conta.saldo_inicial) === 0 ? " conc-valor--zero" : Number(conta.saldo_inicial) < 0 ? " conc-valor--devedor" : ""}">${pidConcSaldo(conta.saldo_inicial)}</td>
<td class="conc-num conc-valor${saldoFinal === 0 ? " conc-valor--zero" : saldoFinal < 0 ? " conc-valor--devedor" : ""}">${pidConcSaldo(conta.saldo_final)}</td>
<td class="conc-num conc-valor${vencido ? " conc-valor--vencido" : " conc-valor--zero"}">${vencido ? pidConcMoeda(vencido) : "—"}</td>
<td>${pidConcStatusHtml(conta.status)}</td>
<td class="conc-validado-col"><input type="checkbox" class="conc-validado-check" data-validado-check ${conta.validada ? "checked" : ""} aria-label="Confirmar que a conta foi validada" title="${pidConcEscape(pidConcTextoValidacao(conta) || "Marcar como validada")}" /></td>`;
const check = tr.querySelector("[data-validado-check]");
// o clique no checkbox não expande/recolhe a linha
check.addEventListener("click", (evento) => evento.stopPropagation());
check.addEventListener("change", async () => {
check.disabled = true;
try {
// sem `observacao` no corpo: o servidor mantém a já registrada
const resposta = await pidApiRequest(`/conciliacoes-fornecedores-contas/${conta.id}/validar/`, {
method: "POST",
body: { validada: check.checked },
});
aplicarRespostaConta(resposta);
} catch (erro) {
check.checked = !check.checked;
check.disabled = false;
pidAlert(erro.message, { titulo: "Não foi possível salvar" });
}
});
tr.addEventListener("click", () => {
if (abertas.has(conta.id)) abertas.delete(conta.id);
else abertas.add(conta.id);
substituirConta(conta);
});
if (!aberta) return [tr];
const trDetalhe = document.createElement("tr");
trDetalhe.className = "conc-detail-row";
trDetalhe.dataset.contaDetalheId = conta.id;
const td = document.createElement("td");
td.colSpan = 8;
td.className = "conc-detail-cell";
td.appendChild(montarDetalheConta(conta));
trDetalhe.appendChild(td);
return [tr, trDetalhe];
}
/** Re-renderiza só as linhas de uma conta (a principal e o detalhe), no lugar. */
function substituirConta(conta) {
const antiga = contasBody.querySelector(`tr[data-conta-id="${conta.id}"]`);
if (!antiga) return;
const detalheAntigo = contasBody.querySelector(`tr[data-conta-detalhe-id="${conta.id}"]`);
const novas = montarLinhasConta(conta);
antiga.replaceWith(...novas);
if (detalheAntigo) detalheAntigo.remove();
}
function aplicarRespostaConta(resposta) {
const indice = detalhe.contas.findIndex((c) => c.id === resposta.conta.id);
const eraValidada = indice >= 0 && detalhe.contas[indice].validada;
if (indice >= 0) detalhe.contas[indice] = resposta.conta;
// Conta que acabou de ser validada (checkbox, botão ou validação
// automática ao ficar conciliada) recolhe a expansão: o trabalho nela
// terminou (pedido do usuário).
if (!eraValidada && resposta.conta.validada) abertas.delete(resposta.conta.id);
detalhe.resumo = resposta.resumo;
const estado = estadoDe(resposta.conta.id);
estado.selecionados.clear();
renderCards();
substituirConta(resposta.conta);
}
function linhaLancamentoHtml(lanc, comCheckbox, selecionado) {
const situacao = lanc.situacao;
const valorAberto = pidConcValorAbertoHtml(situacao);
const historico = lanc.eh_saldo_anterior ? "<em>Saldo anterior</em>" : pidConcEscape(lanc.historico);
return `
<tr class="${selecionado ? "is-selecionado" : ""}" data-lanc-id="${lanc.id}">
${comCheckbox ? `<td class="conc-subtabela__check"><input type="checkbox" data-lanc-check="${lanc.id}" ${selecionado ? "checked" : ""} aria-label="Selecionar lançamento" /></td>` : ""}
<td>${pidConcData(lanc.data)}</td>
<td>${pidConcEscape(lanc.sequencia || "—")}</td>
<td class="conc-historico">${historico}${lanc.contrapartida ? `<span class="conc-historico__contrapartida">${pidConcEscape(lanc.contrapartida)}</span>` : ""}</td>
<td><span class="conc-natureza conc-natureza--${lanc.natureza}">${lanc.natureza === "C" ? "Crédito" : "Débito"}</span></td>
<td class="conc-num conc-valor">${pidConcMoeda(lanc.valor)}</td>
${comCheckbox ? `<td class="conc-num conc-valor">${valorAberto}</td>${pidConcSituacaoHtml(situacao)}` : ""}
</tr>`;
}
function montarDetalheConta(conta) {
const estado = estadoDe(conta.id);
const painel = document.createElement("div");
painel.className = "conc-detail";
if (conta.alertas.length) {
const lista = document.createElement("ul");
lista.className = "conc-alertas";
lista.innerHTML = conta.alertas
.map(
(a) =>
`<li class="conc-alerta conc-alerta--${a.severidade}"><span class="conc-alerta__severidade">${PID_CONC_SEVERIDADE[a.severidade]}</span><span>${pidConcEscape(a.mensagem)}</span></li>`
)
.join("");
painel.appendChild(lista);
}
// ---- Razão desde a primeira pendência ----
// Pedido do usuário: mostrar o razão inteiro a partir do lançamento
// pendente mais antigo (inclusive o que já foi vinculado nesse período),
// para enxergar vínculos automáticos errados e desfazê-los ali mesmo.
// "Pendente" inclui os itens de um pagamento parcial, cujo título segue
// em aberto.
const pendentes = conta.lancamentos.filter((l) => l.vinculo_id === null);
const numeroVinculo = new Map(conta.vinculos.map((v, i) => [v.id, i + 1]));
const vinculoPorId = new Map(conta.vinculos.map((v) => [v.id, v]));
const idsParciais = new Set(conta.vinculos.filter((v) => v.tipo === "parcial").map((v) => v.id));
const datasPendencia = conta.lancamentos
.filter((l) => l.vinculo_id === null || idsParciais.has(l.vinculo_id))
.map((l) => l.data);
const inicioPendencia = datasPendencia.length ? datasPendencia.reduce((a, b) => (a < b ? a : b)) : null;
const razao = inicioPendencia ? conta.lancamentos.filter((l) => l.data >= inicioPendencia) : [];
const qtdVinculadosRazao = razao.filter((l) => l.vinculo_id !== null).length;
const secaoPend = document.createElement("div");
secaoPend.innerHTML = `
<div class="conc-detail__secao-titulo conc-selecao-barra">
<div class="conc-detail__titulo-grupo">
<h4>Razão desde ${inicioPendencia ? pidConcData(inicioPendencia) : "—"} · ${pendentes.length} não conciliado(s)</h4>
${qtdVinculadosRazao ? `<label class="conc-toggle"><input type="checkbox" data-ocultar-conciliados ${estado.ocultarConciliados ? "checked" : ""} /> Ocultar conciliados <span class="conc-chip__count">${qtdVinculadosRazao}</span></label>` : ""}
</div>
<div class="conc-detail__acoes">
<span class="conc-selecao-resumo" data-selecao-resumo></span>
<button type="button" class="btn-solid conc-btn-sm" data-vincular disabled>Vincular selecionados</button>
</div>
</div>`;
if (razao.length) {
const tabela = document.createElement("table");
tabela.className = "conc-subtabela conc-subtabela--pendentes";
tabela.classList.toggle("is-ocultando-conciliados", Boolean(estado.ocultarConciliados));
tabela.innerHTML = `
<thead><tr><th class="conc-subtabela__check"><input type="checkbox" data-lanc-todos aria-label="Marcar ou desmarcar todos os não conciliados" title="Marcar todos" /></th><th>Data</th><th>Seq.</th><th>Histórico</th><th>Natureza</th><th class="conc-num">Valor</th><th class="conc-num">Saldo</th><th class="conc-num">Em aberto</th><th>Situação</th></tr></thead>
<tbody>${razao.map((l) => linhaRazaoHtml(l)).join("")}</tbody>`;
secaoPend.appendChild(tabela);
} else {
secaoPend.insertAdjacentHTML("beforeend", '<p class="conc-sem-pendencias">Todos os lançamentos desta conta estão vinculados.</p>');
}
painel.appendChild(secaoPend);
function linhaRazaoHtml(lanc) {
const historico = lanc.eh_saldo_anterior ? "<em>Saldo anterior</em>" : pidConcEscape(lanc.historico);
const contrapartida = lanc.contrapartida ? `<span class="conc-historico__contrapartida">${pidConcEscape(lanc.contrapartida)}</span>` : "";
const comum = `
<td>${pidConcData(lanc.data)}</td>
<td>${pidConcEscape(lanc.sequencia || "—")}</td>
<td class="conc-historico">${historico}${contrapartida}</td>
<td><span class="conc-natureza conc-natureza--${lanc.natureza}">${lanc.natureza === "C" ? "Crédito" : "Débito"}</span></td>
<td class="conc-num conc-valor">${pidConcMoeda(lanc.valor)}</td>
<td class="conc-num conc-valor conc-valor--saldo">${lanc.saldo_apos === null ? "—" : pidConcSaldo(lanc.saldo_apos)}</td>`;
if (lanc.vinculo_id === null) {
const selecionado = estado.selecionados.has(lanc.id);
return `
<tr class="${selecionado ? "is-selecionado" : ""}" data-lanc-id="${lanc.id}">
<td class="conc-subtabela__check"><input type="checkbox" data-lanc-check="${lanc.id}" ${selecionado ? "checked" : ""} aria-label="Selecionar lançamento" /></td>
${comum}
<td class="conc-num conc-valor">${pidConcValorAbertoHtml(lanc.situacao)}</td>
${pidConcSituacaoHtml(lanc.situacao)}
</tr>`;
}
const vinculo = vinculoPorId.get(lanc.vinculo_id);
const origem = vinculo && (vinculo.tipo === "manual" || vinculo.tipo === "parcial") ? "manual" : "automático";
return `
<tr class="conc-linha--vinculada" data-lanc-id="${lanc.id}" data-vinculo-id="${lanc.vinculo_id}">
<td class="conc-subtabela__check"></td>
${comum}
<td class="conc-num conc-valor">—</td>
<td class="conc-situacao">
<span class="conc-vinculado">
<span class="conc-vinculado-tag">Vinculado #${numeroVinculo.get(lanc.vinculo_id) || "?"}</span>
<span class="conc-vinculado__tipo">${pidConcEscape(vinculo ? vinculo.tipo_rotulo : "")} · ${origem}</span>
<button type="button" class="conc-link-btn" data-desfazer-vinculo="${lanc.vinculo_id}">Desfazer</button>
</span>
</td>
</tr>`;
}
async function desfazerVinculo(vinculoId) {
const itens = conta.lancamentos.filter((l) => l.vinculo_id === vinculoId);
const lista = itens
.map((l) => `${pidConcData(l.data)} ${l.natureza === "C" ? "crédito" : "débito"} ${pidConcMoeda(l.valor)}`)
.join("; ");
const confirmado = await pidConfirm(
`Desfazer o vínculo #${numeroVinculo.get(vinculoId) || ""} (${lista})? Os lançamentos voltam para não conciliados.`,
{ titulo: "Desfazer vínculo", textoConfirmar: "Desfazer" }
);
if (!confirmado) return;
try {
const resposta = await pidApiRequest(`/conciliacoes-fornecedores-vinculos/${vinculoId}/`, { method: "DELETE" });
aplicarRespostaConta(resposta);
} catch (erro) {
pidAlert(erro.message, { titulo: "Não foi possível desfazer" });
}
}
// Linhas vinculadas: destacar os outros itens do mesmo vínculo ao passar
// o mouse, e "Desfazer" direto na linha.
secaoPend.querySelectorAll("tr[data-vinculo-id]").forEach((tr) => {
const grupo = () => secaoPend.querySelectorAll(`tr[data-vinculo-id="${tr.dataset.vinculoId}"]`);
tr.addEventListener("mouseenter", () => grupo().forEach((g) => g.classList.add("is-grupo")));
tr.addEventListener("mouseleave", () => grupo().forEach((g) => g.classList.remove("is-grupo")));
});
// "Ocultar conciliados": desmarcado por padrão (pedido do usuário); a
// escolha fica no estado da conta e sobrevive ao re-render após um vínculo.
const ocultarCheck = secaoPend.querySelector("[data-ocultar-conciliados]");
if (ocultarCheck) {
ocultarCheck.addEventListener("click", (evento) => evento.stopPropagation());
ocultarCheck.addEventListener("change", () => {
estado.ocultarConciliados = ocultarCheck.checked;
const tabelaRazao = secaoPend.querySelector(".conc-subtabela--pendentes");
if (tabelaRazao) tabelaRazao.classList.toggle("is-ocultando-conciliados", ocultarCheck.checked);
});
}
secaoPend.querySelectorAll("[data-desfazer-vinculo]").forEach((btn) => {
btn.addEventListener("click", (evento) => {
evento.stopPropagation();
desfazerVinculo(Number(btn.dataset.desfazerVinculo));
});
});
const resumoSelecaoEl = secaoPend.querySelector("[data-selecao-resumo]");
const todosCheck = secaoPend.querySelector("[data-lanc-todos]");
const vincularBtn = secaoPend.querySelector("[data-vincular]");
const porId = new Map(conta.lancamentos.map((l) => [l.id, l]));
function atualizarSelecao() {
const selecionados = Array.from(estado.selecionados).map((id) => porId.get(id)).filter(Boolean);
const creditos = selecionados.filter((l) => l.natureza === "C").reduce((t, l) => t + Number(l.valor), 0);
const debitos = selecionados.filter((l) => l.natureza === "D").reduce((t, l) => t + Number(l.valor), 0);
const temAmbos = selecionados.some((l) => l.natureza === "C") && selecionados.some((l) => l.natureza === "D");
vincularBtn.disabled = !temAmbos;
painel.querySelectorAll("[data-adicionar]").forEach((btn) => {
btn.disabled = selecionados.length === 0;
});
if (todosCheck) {
todosCheck.checked = selecionados.length > 0 && selecionados.length === pendentes.length;
todosCheck.indeterminate = selecionados.length > 0 && selecionados.length < pendentes.length;
todosCheck.title = selecionados.length ? "Desmarcar todos" : "Marcar todos";
}
resumoSelecaoEl.innerHTML = selecionados.length
? `Créditos <strong>${pidConcMoeda(creditos)}</strong> · Débitos <strong>${pidConcMoeda(debitos)}</strong> · Diferença <strong>${pidConcMoeda(creditos - debitos)}</strong>`
: "Selecione débitos e créditos para vincular manualmente.";
return { selecionados, diferenca: Math.round((creditos - debitos) * 100) / 100 };
}
atualizarSelecao();
// Cabeçalho: sem nada marcado, marca todos; com algum marcado (parcial ou
// todos), desmarca todos.
if (todosCheck) {
todosCheck.addEventListener("change", () => {
const marcar = estado.selecionados.size === 0;
secaoPend.querySelectorAll("[data-lanc-check]").forEach((check) => {
check.checked = marcar;
check.closest("tr").classList.toggle("is-selecionado", marcar);
const id = Number(check.dataset.lancCheck);
if (marcar) estado.selecionados.add(id);
else estado.selecionados.delete(id);
});
atualizarSelecao();
});
}
// A linha inteira marca/desmarca (pedido do usuário); o checkbox fica
// como indicador. Clique no próprio checkbox segue o comportamento
// nativo, e arrastar para selecionar texto não conta como clique.
secaoPend.querySelectorAll(".conc-subtabela--pendentes tbody tr[data-lanc-id]:not([data-vinculo-id])").forEach((tr) => {
tr.addEventListener("click", (evento) => {
if (evento.target.closest("[data-lanc-check]")) return;
if (String(window.getSelection ? window.getSelection() : "").trim()) return;
const check = tr.querySelector("[data-lanc-check]");
check.checked = !check.checked;
check.dispatchEvent(new Event("change"));
});
});
secaoPend.querySelectorAll("[data-lanc-check]").forEach((check) => {
check.addEventListener("change", () => {
const id = Number(check.dataset.lancCheck);
if (check.checked) estado.selecionados.add(id);
else estado.selecionados.delete(id);
check.closest("tr").classList.toggle("is-selecionado", check.checked);
atualizarSelecao();
});
});
vincularBtn.addEventListener("click", async () => {
const { selecionados, diferenca } = atualizarSelecao();
// Mesmo critério do servidor (vincular/): créditos maiores que os
// débitos além da tolerância = pagamento parcial, o título continua em
// aberto pelo saldo.
const totalCreditos = selecionados.filter((l) => l.natureza === "C").reduce((t, l) => t + Number(l.valor), 0);
const limite = Math.max(Number(detalhe.tolerancia_valor), (totalCreditos * Number(detalhe.tolerancia_percentual)) / 100);
if (diferenca > limite) {
const seguir = await pidConfirm(
`Os débitos selecionados pagam ${pidConcMoeda(totalCreditos - diferenca)} de ${pidConcMoeda(totalCreditos)}. Vincular como pagamento parcial? O título continua em aberto com ${pidConcMoeda(diferenca)}.`,
{ titulo: "Pagamento parcial", textoConfirmar: "Vincular" }
);
if (!seguir) return;
} else if (diferenca !== 0) {
const natureza = diferenca < 0 ? "pago a maior (possível juros/multa)" : "pago a menor (possível desconto)";
const seguir = await pidConfirm(
`Os lançamentos selecionados têm diferença de ${pidConcMoeda(Math.abs(diferenca))} (${natureza}). Vincular mesmo assim? O vínculo fica sinalizado nos alertas da conta.`,
{ titulo: "Vínculo com diferença", textoConfirmar: "Vincular" }
);
if (!seguir) return;
}
vincularBtn.disabled = true;
try {
const resposta = await pidApiRequest(`/conciliacoes-fornecedores-contas/${conta.id}/vincular/`, {
method: "POST",
body: { lancamentos: selecionados.map((l) => l.id) },
});
aplicarRespostaConta(resposta);
} catch (erro) {
vincularBtn.disabled = false;
pidAlert(erro.message, { titulo: "Não foi possível vincular" });
}
});
// ---- Vinculados ----
function montarBlocoVinculo(vinculo) {
const itens = conta.lancamentos.filter((l) => l.vinculo_id === vinculo.id);
const manual = vinculo.tipo === "manual" || vinculo.tipo === "parcial";
const origem = manual ? `manual, por ${pidConcEscape(vinculo.criado_por_nome || "—")}` : "automático";
const diferenca = Number(vinculo.diferenca);
let detalheDiferenca = "";
if (vinculo.tipo === "parcial") {
const saldo = vinculo.situacao;
detalheDiferenca = ` · restam <strong>${saldo ? pidConcValorAbertoHtml(saldo) : pidConcMoeda(diferenca)}</strong> em aberto${saldo ? ` · ${pidConcEscape(saldo.descricao)}` : ""}`;
} else if (diferenca) {
detalheDiferenca = ` · diferença ${pidConcMoeda(Math.abs(diferenca))} ${diferenca < 0 ? "(pago a maior)" : "(pago a menor)"}`;
}
const bloco = document.createElement("div");
bloco.className = `conc-vinculo${vinculo.tipo === "parcial" ? " conc-vinculo--parcial" : ""}`;
bloco.innerHTML = `
<div class="conc-vinculo__header">
<span><strong>#${numeroVinculo.get(vinculo.id)} ${pidConcEscape(vinculo.tipo_rotulo)}</strong> · ${origem}${detalheDiferenca}</span>
<span class="conc-vinculo__acoes">
${manual ? '<button type="button" class="btn-outline conc-btn-sm" data-adicionar disabled title="Marque lançamentos na lista de não conciliados">Adicionar selecionados</button>' : ""}
<button type="button" class="btn-outline conc-btn-sm" data-desfazer>Desfazer vínculo</button>
</span>
</div>
<table class="conc-subtabela">
<thead><tr><th>Data</th><th>Seq.</th><th>Histórico</th><th>Natureza</th><th class="conc-num">Valor</th></tr></thead>
<tbody>${itens.map((l) => linhaLancamentoHtml(l, false, false)).join("")}</tbody>
</table>`;
const adicionarBtn = bloco.querySelector("[data-adicionar]");
if (adicionarBtn) {
adicionarBtn.addEventListener("click", async () => {
const novos = Array.from(estado.selecionados).map((id) => porId.get(id)).filter(Boolean);
if (!novos.length) return;
const assinado = (l) => (l.natureza === "C" ? Number(l.valor) : -Number(l.valor));
const novaDiferenca = [...itens, ...novos].reduce((t, l) => t + assinado(l), 0);
const arredondada = Math.round(novaDiferenca * 100) / 100;
const efeito =
arredondada > 0
? `restarão ${pidConcMoeda(arredondada)} de diferença (em aberto, se passar da tolerância)`
: arredondada < 0
? `ficará pago a maior em ${pidConcMoeda(-arredondada)} (possível juros/multa)`
: "o vínculo fecha sem diferença";
const seguir = await pidConfirm(
`Adicionar ${novos.length} lançamento(s) a este vínculo? Com eles, ${efeito}.`,
{ titulo: "Adicionar ao vínculo", textoConfirmar: "Adicionar" }
);
if (!seguir) return;
adicionarBtn.disabled = true;
try {
const resposta = await pidApiRequest(`/conciliacoes-fornecedores-vinculos/${vinculo.id}/adicionar/`, {
method: "POST",
body: { lancamentos: novos.map((l) => l.id) },
});
aplicarRespostaConta(resposta);
} catch (erro) {
adicionarBtn.disabled = false;
pidAlert(erro.message, { titulo: "Não foi possível adicionar" });
}
});
}
bloco.querySelector("[data-desfazer]").addEventListener("click", () => desfazerVinculo(vinculo.id));
return bloco;
}
// Pagamento parcial não é conciliação fechada: fica junto dos não
// conciliados, sempre visível, com o saldo que continua em aberto.
const parciais = conta.vinculos.filter((v) => v.tipo === "parcial");
if (parciais.length) {
const secaoParcial = document.createElement("div");
secaoParcial.innerHTML = `<div class="conc-detail__secao-titulo"><h4>Pagamentos parciais vinculados (${parciais.length})</h4></div>`;
parciais.forEach((v) => secaoParcial.appendChild(montarBlocoVinculo(v)));
secaoPend.after(secaoParcial);
}
const fechados = conta.vinculos.filter((v) => v.tipo !== "parcial");
if (fechados.length) {
const secaoVinc = document.createElement("div");
secaoVinc.innerHTML = `
<div class="conc-detail__secao-titulo">
<h4>Lançamentos conciliados (${fechados.length} vínculo(s))</h4>
<button type="button" class="btn-outline conc-btn-sm" data-toggle-vinculados>${estado.mostrarVinculados ? "Ocultar" : "Mostrar"}</button>
</div>`;
secaoVinc.querySelector("[data-toggle-vinculados]").addEventListener("click", () => {
estado.mostrarVinculados = !estado.mostrarVinculados;
substituirConta(conta);
});
if (estado.mostrarVinculados) {
const lista = document.createElement("div");
lista.className = "conc-vinculos-lista";
fechados
.slice()
.reverse()
.forEach((vinculo) => lista.appendChild(montarBlocoVinculo(vinculo)));
secaoVinc.appendChild(lista);
}
painel.appendChild(secaoVinc);
}
// os botões "Adicionar selecionados" dos vínculos nascem depois da
// primeira atualização da seleção
atualizarSelecao();
// ---- Validação ----
const validacao = document.createElement("div");
validacao.className = "conc-validacao";
validacao.innerHTML = `
<label class="conc-detail__secao-titulo"><h4>Observação do contador</h4></label>
<textarea data-observacao placeholder="Registre a análise desta conta (ex.: título já pago em outubro, aguardando baixa).">${pidConcEscape(conta.observacao)}</textarea>
<div class="conc-detail__acoes">
<button type="button" class="btn-outline conc-btn-sm" data-salvar-obs>Salvar observação</button>
<button type="button" class="${conta.validada ? "btn-outline" : "btn-solid"} conc-btn-sm" data-validar>${conta.validada ? "Remover validação" : "Marcar como validada"}</button>
${conta.validada ? `<span class="conc-validacao__info">${pidConcEscape(pidConcTextoValidacao(conta))}</span>` : ""}
<button type="button" class="btn-outline conc-btn-sm conc-recolher-btn" data-recolher>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 15l6-6 6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
Recolher
</button>
</div>`;
const observacaoEl = validacao.querySelector("[data-observacao]");
async function salvarValidacao(validada) {
try {
const resposta = await pidApiRequest(`/conciliacoes-fornecedores-contas/${conta.id}/validar/`, {
method: "POST",
body: { validada, observacao: observacaoEl.value },
});
aplicarRespostaConta(resposta);
} catch (erro) {
pidAlert(erro.message, { titulo: "Não foi possível salvar" });
}
}
validacao.querySelector("[data-salvar-obs]").addEventListener("click", () => salvarValidacao(conta.validada));
validacao.querySelector("[data-validar]").addEventListener("click", () => salvarValidacao(!conta.validada));
validacao.querySelector("[data-recolher]").addEventListener("click", () => {
abertas.delete(conta.id);
substituirConta(conta);
// a expansão pode ser longa: volta a tela para a linha do fornecedor
const linha = contasBody.querySelector(`tr[data-conta-id="${conta.id}"]`);
if (linha && linha.getBoundingClientRect().top < 120) linha.scrollIntoView({ block: "center" });
});
painel.appendChild(validacao);
return painel;
}
// ======================================================================
// Início
// ======================================================================
const hash = window.location.hash.match(/^#conciliacao-(\d+)$/);
if (hash) {
abrirDetalhe(Number(hash[1]));
} else {
carregarHistorico();
}
});