portal_publico/static/js/indicador-desempenho.js

2202 lines
99 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const PID_IND_TIPO_LABELS = {
1: "Contábil + Fiscal",
2: "Contador (sem conciliador)",
3: "Contador (com conciliador)",
4: "Fiscal",
5: "Conciliador",
};
const PID_IND_GRUPO_LABELS = { individual: "Individual", grupo: "Grupo", departamento: "Departamento" };
const PID_IND_PERIODO_LABELS = { todos: "Todos os meses", mar_a_nov: "Março a Novembro", dez_a_jan: "Dezembro a Janeiro" };
const PID_IND_CALCULO_LABELS = {
"": "Manual",
balancete: "Balancetes (automático)",
liberacao_fiscal: "Liberações fiscais (automático)",
conciliacao: "Conciliações (automático)",
};
const PID_IND_STATUS_LABELS = { revisao: "Em revisão", concluida: "Concluída" };
function pidIndFormatMoeda(valor) {
const numero = Number(valor || 0);
return numero.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
}
function pidIndFormatPercentual(valor) {
return `${Number(valor || 0).toFixed(2)}%`;
}
// Peso é configurado pelo RH em IndicadorCriterio (ex.: 60, 10, 30) — quase
// sempre um número redondo; formata sem casas decimais "penduradas" (60% em
// vez de 60.00%), mas preserva casas reais se um peso fracionário existir.
function pidIndFormatPeso(valor) {
return `${parseFloat(Number(valor || 0).toFixed(2))}%`;
}
// Todo percentual "atingido" deste app (Individual/Grupo/Departamento, cada
// um é uma média ponderada de critérios/percentuais <= 100) só bate 100,00%
// quando tudo que pesa ali foi de fato atingido — mesmo critério usado pra
// colorir cada um dos 3 níveis da composição (verde/vermelho).
function pidIndPercentualAtingido(valor) {
return Number(valor || 0).toFixed(2) === "100.00";
}
function pidIndFormatCompetencia(iso) {
if (!iso) return "";
const [ano, mes] = iso.split("-");
return `${mes}/${ano}`;
}
function pidIndMascaraCompetencia(valor) {
const digitos = (valor || "").replace(/\D/g, "").slice(0, 6);
if (digitos.length <= 2) return digitos;
return `${digitos.slice(0, 2)}/${digitos.slice(2)}`;
}
function pidIndCompetenciaParaIso(valor) {
const match = /^(\d{2})\/(\d{4})$/.exec(valor || "");
if (!match) return null;
const mes = Number(match[1]);
if (mes < 1 || mes > 12) return null;
return `${match[2]}-${match[1]}-01`;
}
function pidIndPeriodoAtual(competenciaIso) {
// Mesma regra de IndicadorApuracao.periodo_criterio() (models.py): dezembro
// e janeiro caem no período "Dez a Jan", os outros 10 meses em "Mar a Nov".
const mes = Number((competenciaIso || "").split("-")[1]);
return mes === 12 || mes === 1 ? "dez_a_jan" : "mar_a_nov";
}
function pidIndRespostaNaoUtilizada(resposta, colaborador, periodoAtual) {
if (resposta.valor === "NAO_SE_APLICA") return true;
if (resposta.criterio_periodo !== "todos" && resposta.criterio_periodo !== periodoAtual) return true;
if (resposta.criterio_grupo === "individual" && resposta.criterio_papel_aplicavel) {
const tiposDoColaborador = new Set(colaborador.empresas.map((empresa) => empresa.tipo));
if (!tiposDoColaborador.has(resposta.criterio_papel_aplicavel)) return true;
}
return false;
}
function pidIndFormatData(iso) {
if (!iso) return "";
return new Date(iso).toLocaleDateString("pt-BR");
}
function pidIndEscapeHtml(texto) {
const div = document.createElement("div");
div.textContent = texto == null ? "" : String(texto);
return div.innerHTML;
}
function pidIndEscapeAttr(texto) {
return String(texto == null ? "" : texto)
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;");
}
// "Cada gerente representa um grupo" (CLAUDE.md): agrupa os colaboradores da
// apuração por `gerente`, ordenado alfabeticamente — quem não tem gerente
// preenchido cai num grupo próprio, sempre exibido por último.
function pidIndAgruparPorGerente(colaboradores) {
const grupos = new Map();
colaboradores.forEach((colaborador) => {
const chave = colaborador.gerente || "";
if (!grupos.has(chave)) grupos.set(chave, []);
grupos.get(chave).push(colaborador);
});
return Array.from(grupos.entries()).sort(([a], [b]) => {
if (!a && !b) return 0;
if (!a) return 1;
if (!b) return -1;
return a.localeCompare(b, "pt-BR");
});
}
// Sentinel usado quando o `gerente` do colaborador ainda não está mapeado a
// nenhum IndicadorDepartamento (ver Configurações → Departamentos) — grupo
// próprio, sempre exibido por último.
const PID_IND_SEM_DEPARTAMENTO = "__sem-departamento__";
function pidIndChaveDepartamento(departamentoId) {
return departamentoId == null ? PID_IND_SEM_DEPARTAMENTO : String(departamentoId);
}
// Cada IndicadorDepartamento (Fisco/Contábil, Rocket, Gerentes, ...) tem sua
// própria meta de Departamento, seus próprios critérios e percentuais (ver
// CLAUDE.md) — agrupa os colaboradores da apuração por `departamento` (id,
// resolvido no servidor a partir do gerente na criação da apuração),
// ordenado alfabeticamente pelo nome do departamento.
function pidIndAgruparPorDepartamento(colaboradores) {
const grupos = new Map();
colaboradores.forEach((colaborador) => {
const chave = pidIndChaveDepartamento(colaborador.departamento);
if (!grupos.has(chave)) {
grupos.set(chave, {
chave,
id: colaborador.departamento,
nome: colaborador.departamento == null ? "Sem departamento definido" : colaborador.departamento_nome,
colaboradores: [],
});
}
grupos.get(chave).colaboradores.push(colaborador);
});
return Array.from(grupos.values()).sort((a, b) => {
if (a.id == null) return 1;
if (b.id == null) return -1;
return a.nome.localeCompare(b.nome, "pt-BR");
});
}
// Grupo/Departamento são compartilhados entre colaboradores (ver CLAUDE.md) —
// como o valor efetivo ainda é gravado por colaborador no banco (sem tabela
// nova pra grupo/apuração), aqui tiramos a média do conjunto pra exibição:
// depois de qualquer ajuste em bloco (ajustar-grupo/ajustar-departamento)
// todos os membros já ficam com o mesmo valor, então a média coincide com
// ele; só diverge se as respostas de critério subjacentes (ainda editáveis
// por colaborador) não tiverem sido igualadas — nesse caso a média é o
// retrato mais honesto do grupo até alguém usar o ajuste em bloco.
function pidIndValorRepresentativo(colaboradores, campo) {
if (!colaboradores.length) return { valor: 100, ajustado: false };
const soma = colaboradores.reduce((total, colaborador) => total + Number(colaborador[campo]), 0);
return {
valor: soma / colaboradores.length,
ajustado: colaboradores.some((colaborador) => colaborador[`${campo}_ajustado_manualmente`]),
};
}
// ---- API ----
async function pidFetchApuracoesIndicador() {
return pidApiRequest("/indicadores-apuracoes/");
}
async function pidFetchApuracaoIndicador(id) {
return pidApiRequest(`/indicadores-apuracoes/${id}/`);
}
async function pidCriarApuracaoIndicador(formData) {
return pidApiRequest("/indicadores-apuracoes/", { method: "POST", body: formData });
}
async function pidExcluirApuracaoIndicador(id) {
return pidApiRequest(`/indicadores-apuracoes/${id}/`, { method: "DELETE" });
}
async function pidAtualizarRespostaIndicador(id, valor) {
return pidApiRequest(`/indicadores-apuracoes-respostas/${id}/`, { method: "PATCH", body: { valor } });
}
async function pidAjustarPercentualColaboradorIndicador(colaboradorId, campo, valor) {
return pidApiRequest(`/indicadores-apuracoes-colaboradores/${colaboradorId}/`, {
method: "PATCH",
body: { [campo]: valor },
});
}
async function pidRecalcularColaboradorIndicador(colaboradorId) {
return pidApiRequest(`/indicadores-apuracoes-colaboradores/${colaboradorId}/recalcular/`, { method: "POST" });
}
// Checklist de revisão do RH (`validado`) — não recalcula nada, só marca/
// desmarca que este colaborador já foi conferido.
async function pidMarcarValidadoColaboradorIndicador(colaboradorId, validado) {
return pidApiRequest(`/indicadores-apuracoes-colaboradores/${colaboradorId}/marcar-validado/`, {
method: "POST",
body: { validado },
});
}
async function pidAjustarHonorarioEmpresaIndicador(empresaId, honorario) {
return pidApiRequest(`/indicadores-apuracoes-empresas/${empresaId}/`, { method: "PATCH", body: { honorario } });
}
// Reatribui uma linha (empresa+tipo) — identificada por `empresaId`, o id da
// IndicadorApuracaoEmpresa — pra outro colaborador da mesma apuração.
async function pidTrocarResponsavelEmpresaIndicador(empresaId, colaboradorId) {
return pidApiRequest(`/indicadores-apuracoes-empresas/${empresaId}/trocar-responsavel/`, {
method: "POST",
body: { colaborador_id: colaboradorId },
});
}
// "Grupo" (cada gerente representa um grupo) e "Departamento" (todo o
// Fiscontábil) não são ajustados colaborador a colaborador — os 4 endpoints
// abaixo aplicam o valor/recálculo de uma vez a todos os colaboradores do
// mesmo gerente (grupo) ou de toda a apuração (departamento), ver CLAUDE.md.
async function pidAjustarGrupoIndicador(apuracaoId, gerente, pctGrupo) {
return pidApiRequest(`/indicadores-apuracoes/${apuracaoId}/ajustar-grupo/`, {
method: "POST",
body: { gerente, pct_grupo: pctGrupo },
});
}
async function pidRecalcularGrupoIndicador(apuracaoId, gerente) {
return pidApiRequest(`/indicadores-apuracoes/${apuracaoId}/recalcular-grupo/`, {
method: "POST",
body: { gerente },
});
}
async function pidAjustarDepartamentoIndicador(apuracaoId, departamentoId, pctDepartamento) {
return pidApiRequest(`/indicadores-apuracoes/${apuracaoId}/ajustar-departamento/`, {
method: "POST",
body: { departamento: departamentoId, pct_departamento: pctDepartamento },
});
}
async function pidRecalcularDepartamentoIndicador(apuracaoId, departamentoId) {
return pidApiRequest(`/indicadores-apuracoes/${apuracaoId}/recalcular-departamento/`, {
method: "POST",
body: { departamento: departamentoId },
});
}
// Preenche o honorário de uma empresa (por código) de uma vez pra todos os
// colaboradores desta apuração que a têm com honorario_nao_encontrado=True —
// diferente de pidAjustarHonorarioEmpresaIndicador (acima), que ajusta só uma
// linha por id. Ver CLAUDE.md/IndicadorApuracaoViewSet.ajustar_honorario_empresa.
async function pidAjustarHonorarioEmpresaEmLoteIndicador(apuracaoId, codigoEmpresa, honorario) {
return pidApiRequest(`/indicadores-apuracoes/${apuracaoId}/ajustar-honorario-empresa/`, {
method: "POST",
body: { codigo_empresa: codigoEmpresa, honorario },
});
}
async function pidAplicarEmLoteIndicador(respostaIds, valor) {
return pidApiRequest("/indicadores-apuracoes-respostas/aplicar-em-lote/", {
method: "POST",
body: { resposta_ids: respostaIds, valor },
});
}
async function pidFetchCriteriosIndicador() {
return pidApiRequest("/indicadores-criterios/");
}
async function pidCriarCriterioIndicador(dados) {
return pidApiRequest("/indicadores-criterios/", { method: "POST", body: dados });
}
async function pidAtualizarCriterioIndicador(id, dados) {
return pidApiRequest(`/indicadores-criterios/${id}/`, { method: "PATCH", body: dados });
}
async function pidExcluirCriterioIndicador(id) {
return pidApiRequest(`/indicadores-criterios/${id}/`, { method: "DELETE" });
}
async function pidFetchDepartamentosIndicador() {
return pidApiRequest("/indicadores-departamentos/");
}
async function pidCriarDepartamentoIndicador(dados) {
return pidApiRequest("/indicadores-departamentos/", { method: "POST", body: dados });
}
async function pidAtualizarDepartamentoIndicador(id, dados) {
return pidApiRequest(`/indicadores-departamentos/${id}/`, { method: "PATCH", body: dados });
}
async function pidExcluirDepartamentoIndicador(id) {
return pidApiRequest(`/indicadores-departamentos/${id}/`, { method: "DELETE" });
}
async function pidFetchDepartamentoGerentesIndicador() {
return pidApiRequest("/indicadores-departamentos-gerentes/");
}
async function pidCriarDepartamentoGerenteIndicador(dados) {
return pidApiRequest("/indicadores-departamentos-gerentes/", { method: "POST", body: dados });
}
async function pidExcluirDepartamentoGerenteIndicador(id) {
return pidApiRequest(`/indicadores-departamentos-gerentes/${id}/`, { method: "DELETE" });
}
async function pidFetchPercentuaisIndicador() {
return pidApiRequest("/indicadores-percentuais-tipo/");
}
async function pidCriarPercentualIndicador(dados) {
return pidApiRequest("/indicadores-percentuais-tipo/", { method: "POST", body: dados });
}
async function pidExcluirPercentualIndicador(id) {
return pidApiRequest(`/indicadores-percentuais-tipo/${id}/`, { method: "DELETE" });
}
/**
* Resposta de /gerar/ é binária (ZIP de PDFs), não JSON — mesmo padrão de
* pidGerarArquivoPlanoSaude em importacao-plano-saude.js: fetch manual,
* reaproveitando pidEnsureCsrfCookie/pidGetCookie/pidErrorMessageFrom (globais
* de api.js). `colaboradorIds` opcional restringe a geração a só esses
* colaboradores (modal "Gerar Recibos") — omitido/vazio gera pra todos.
*/
async function pidGerarRecibosIndicador(id, colaboradorIds) {
await pidEnsureCsrfCookie();
const response = await fetch(`/api/indicadores-apuracoes/${id}/gerar/`, {
method: "POST",
credentials: "include",
headers: {
"X-CSRFToken": pidGetCookie("csrftoken") || "",
"Content-Type": "application/json",
},
body: JSON.stringify({ colaborador_ids: colaboradorIds && colaboradorIds.length ? colaboradorIds : undefined }),
});
if (!response.ok) {
let mensagem = "Não foi possível gerar os recibos.";
try {
mensagem = pidErrorMessageFrom(await response.json());
} catch (e) {
// resposta sem corpo JSON
}
throw new Error(mensagem);
}
const blob = await response.blob();
const disposition = response.headers.get("Content-Disposition") || "";
const match = disposition.match(/filename="?([^"]+)"?/);
const filename = match ? match[1] : `recibos_indicador_${id}.zip`;
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
document.addEventListener("DOMContentLoaded", async () => {
const listView = document.getElementById("ind-list-view");
if (!listView) return;
const me = await pidGetMe();
if (!me) return;
const geradocPerms = (me.permissoes_efetivas && me.permissoes_efetivas["geradoc"]) || {};
const canUse = !!(geradocPerms.enabled && geradocPerms.apps && geradocPerms.apps["indicador-desempenho"]);
const noAccessEl = document.getElementById("ind-no-access");
const contentEl = document.getElementById("ind-content");
if (noAccessEl) noAccessEl.hidden = canUse;
if (contentEl) contentEl.hidden = !canUse;
if (!canUse) return;
const formView = document.getElementById("ind-form-view");
const reviewView = document.getElementById("ind-review-view");
const configView = document.getElementById("ind-config-view");
let apuracaoAtual = null;
const respostasSelecionadas = new Set();
let criterios = [];
let percentuais = [];
let departamentosConfig = [];
let departamentoGerentesConfig = [];
function showView(view) {
listView.hidden = view !== "list";
formView.hidden = view !== "form";
reviewView.hidden = view !== "review";
configView.hidden = view !== "config";
}
// ---- Histórico ----
async function carregarLista() {
const apuracoes = await pidFetchApuracoesIndicador();
const body = document.getElementById("ind-list-body");
const empty = document.getElementById("ind-list-empty");
body.innerHTML = "";
empty.hidden = apuracoes.length > 0;
apuracoes.forEach((apuracao) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${pidIndFormatCompetencia(apuracao.competencia)}</td>
<td>${apuracao.total_colaboradores}</td>
<td><span class="status-pill ${apuracao.status === "concluida" ? "status-pill--ativo" : "status-pill--inativo"}">${PID_IND_STATUS_LABELS[apuracao.status] || apuracao.status}</span></td>
<td>${pidIndEscapeHtml(apuracao.criado_por_nome || "-")}</td>
<td>${pidIndFormatData(apuracao.criado_em)}</td>
<td class="pa-row-actions">
<button type="button" class="btn-outline" data-ind-abrir="${apuracao.id}">Abrir</button>
<button type="button" class="icon-btn" data-ind-excluir="${apuracao.id}" aria-label="Excluir" title="Excluir">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</td>
`;
body.appendChild(tr);
});
}
document.getElementById("ind-list-body").addEventListener("click", async (event) => {
const abrirBtn = event.target.closest("[data-ind-abrir]");
if (abrirBtn) {
await abrirRevisao(Number(abrirBtn.getAttribute("data-ind-abrir")));
return;
}
const excluirBtn = event.target.closest("[data-ind-excluir]");
if (excluirBtn) {
if (!(await pidConfirm("Excluir esta apuração? Essa ação não pode ser desfeita.", { perigoso: true }))) return;
await pidExcluirApuracaoIndicador(Number(excluirBtn.getAttribute("data-ind-excluir")));
await carregarLista();
}
});
function wireArquivoField(inputId) {
const input = document.getElementById(inputId);
const nameEl = document.getElementById(`${inputId}-name`);
const removeBtn = document.getElementById(`${inputId}-remove`);
function atualizar() {
const arquivo = input.files[0];
nameEl.textContent = arquivo ? arquivo.name : "Nenhum arquivo selecionado";
removeBtn.hidden = !arquivo;
}
input.addEventListener("change", atualizar);
removeBtn.addEventListener("click", () => {
input.value = "";
atualizar();
});
return atualizar;
}
const resetTareffaField = wireArquivoField("ind-form-tareffa");
const resetHonorariosField = wireArquivoField("ind-form-honorarios");
document.getElementById("ind-form-competencia").addEventListener("input", (event) => {
event.target.value = pidIndMascaraCompetencia(event.target.value);
});
document.getElementById("ind-new-btn").addEventListener("click", () => {
document.getElementById("ind-form-competencia").value = "";
document.getElementById("ind-form-tareffa").value = "";
document.getElementById("ind-form-honorarios").value = "";
resetTareffaField();
resetHonorariosField();
document.getElementById("ind-form-error").textContent = "";
showView("form");
});
document.getElementById("ind-form-back-btn").addEventListener("click", () => showView("list"));
document.getElementById("ind-form-cancel-btn").addEventListener("click", () => showView("list"));
document.getElementById("ind-form-submit-btn").addEventListener("click", async () => {
const errorEl = document.getElementById("ind-form-error");
errorEl.textContent = "";
const competenciaIso = pidIndCompetenciaParaIso(document.getElementById("ind-form-competencia").value);
const tareffa = document.getElementById("ind-form-tareffa").files[0];
const honorarios = document.getElementById("ind-form-honorarios").files[0];
if (!competenciaIso || !tareffa || !honorarios) {
errorEl.textContent = "Preencha a competência (MM/AAAA) e anexe as duas planilhas.";
return;
}
const formData = new FormData();
formData.append("competencia", competenciaIso);
formData.append("planilha_tareffa", tareffa);
formData.append("planilha_honorarios", honorarios);
const submitBtn = document.getElementById("ind-form-submit-btn");
submitBtn.disabled = true;
try {
const apuracao = await pidCriarApuracaoIndicador(formData);
apuracaoAtual = apuracao;
renderRevisao();
showView("review");
await carregarLista();
} catch (e) {
errorEl.textContent = e.message;
} finally {
submitBtn.disabled = false;
}
});
// ---- Revisão ----
const PID_IND_FILTRO_TODOS = "__todos__";
let filtroDepartamento = PID_IND_FILTRO_TODOS;
async function abrirRevisao(id) {
apuracaoAtual = await pidFetchApuracaoIndicador(id);
respostasSelecionadas.clear();
filtroDepartamento = PID_IND_FILTRO_TODOS;
renderRevisao();
showView("review");
}
function pidIndRespostasDoGrupo(colaborador, grupo) {
// Mostra todos os critérios ativos daquele grupo, mesmo os que não se
// aplicam ao(s) papel(is) do colaborador nesta apuração — a exclusão por
// papel/período só afeta o cálculo do percentual (ver calculo.py), não a
// visibilidade na revisão, pra ficar transparente o que foi considerado.
return colaborador.respostas.filter((resposta) => resposta.criterio_grupo === grupo);
}
function renderRevisao() {
const apuracao = apuracaoAtual;
document.getElementById("ind-review-subtitle").textContent = `Competência ${pidIndFormatCompetencia(apuracao.competencia)} — ${apuracao.colaboradores.length} colaborador(es)`;
document.getElementById("ind-review-error").textContent = "";
const avisosEl = document.getElementById("ind-review-avisos");
if (apuracao.avisos && apuracao.avisos.length) {
avisosEl.hidden = false;
avisosEl.innerHTML = `<strong>Casos pra confirmar manualmente:</strong><ul>${apuracao.avisos
.map((aviso) => `<li>${pidIndEscapeHtml(aviso)}</li>`)
.join("")}</ul>`;
} else {
avisosEl.hidden = true;
avisosEl.innerHTML = "";
}
renderFiltroDepartamento();
renderMetasSection();
renderColaboradoresList();
atualizarBotaoEmpresasSemHonorario();
atualizarBarraLote();
}
// ---- Metas de Grupo e Departamento (tela no início da revisão) ----
// "Cada gerente representa um grupo" (CLAUDE.md): em vez de editar o
// percentual espalhado card a card, o RH preenche todas as metas — uma por
// gerente + a do Departamento (todo o Fiscontábil) — numa única tabela no
// topo da página, antes da lista de colaboradores.
// Mostra, embaixo de cada label da tabela de metas, os mesmos critérios que
// compõem aquele nível (mesma fonte usada na seção "Individual"/"Grupo"/
// "Departamento" de dentro do card do colaborador — ver
// renderRespostasGrupoHtml) — deixa explícito o que o percentual
// preenchido está medindo, do mesmo jeito que a linha "Critério" já deixa
// claro na parte individual.
// `colaboradoresEscopo` é quem responde esse critério — todos os
// colaboradores da apuração pra Departamento, só os do gerente pra Grupo —
// usa o primeiro deles como referência pro valor atual do select (todos
// deveriam estar em sincronia, já que selecionar aqui aplica em lote pra
// todo o escopo de uma vez).
function pidIndCriteriosDoGrupo(grupo, colaboradoresEscopo) {
const primeiroColaborador = colaboradoresEscopo[0];
if (!primeiroColaborador) return [];
const periodoAtual = pidIndPeriodoAtual(apuracaoAtual.competencia);
return primeiroColaborador.respostas
.filter((resposta) => resposta.criterio_grupo === grupo)
.map((resposta) => ({
criterioId: resposta.criterio,
nome: resposta.criterio_nome,
peso: resposta.criterio_peso,
valor: resposta.valor,
apagado: resposta.criterio_periodo !== "todos" && resposta.criterio_periodo !== periodoAtual,
}));
}
// Só texto informativo agora (nome + peso do critério) — o Sim/Não de cada
// critério deixou de ser respondível aqui (existia um select duplicado ao
// lado do texto, redundante com o Sim/Não da coluna "Meta (%)" à direita,
// que já ajusta pct_grupo/pct_departamento direto; pedido explícito do
// usuário pra remover a duplicidade). Responder um critério específico
// continua possível por colaborador, dentro do card de revisão (ver
// renderRespostasGrupoHtml).
function renderMetaCriteriosHtml(criteriosDoGrupo) {
if (!criteriosDoGrupo.length) {
return '<p class="ind-meta-criterios-vazio">Nenhum critério cadastrado pra este nível.</p>';
}
return `
<ul class="ind-meta-criterios">
${criteriosDoGrupo
.map(
(criterio) => `
<li class="${criterio.apagado ? "is-apagado" : ""}">
<span class="ind-meta-criterios__nome">${pidIndEscapeHtml(criterio.nome)}</span>
<span class="ind-meta-criterios__peso">(peso ${pidIndFormatPercentual(criterio.peso)})</span>
${criterio.apagado ? '<span class="ind-apagado-badge">não entra no cálculo neste período</span>' : ""}
</li>`
)
.join("")}
</ul>
`;
}
// Uma linha de Departamento por IndicadorDepartamento (Fisco/Contábil,
// Rocket, Gerentes, ...) + as linhas de Grupo (por gerente) dos
// colaboradores daquele departamento — o filtro de departamento
// (`renderFiltroDepartamento`) restringe a só um de cada vez, sem afetar o
// cálculo de nenhum deles (ver CLAUDE.md). Colaborador cujo gerente ainda
// não está mapeado a nenhum departamento cai num grupo "Sem departamento
// definido", sem `<select>` de meta (não há em qual IndicadorDepartamento
// aplicá-la).
function renderMetasSection() {
const body = document.getElementById("ind-metas-body");
const gruposDepartamento = pidIndAgruparPorDepartamento(apuracaoAtual.colaboradores).filter(
(grupo) => filtroDepartamento === PID_IND_FILTRO_TODOS || grupo.chave === filtroDepartamento
);
const linhas = [];
gruposDepartamento.forEach((grupo) => {
const colaboradoresDoDepartamento = grupo.colaboradores;
if (grupo.id == null) {
linhas.push(`
<tr>
<td>
<div class="ind-meta-label">${pidIndEscapeHtml(grupo.nome)}</div>
<p class="ind-meta-sem-departamento-aviso">Gerente não mapeado a nenhum departamento — cadastre em Configurações → Departamentos.</p>
</td>
<td>${colaboradoresDoDepartamento.length} colaborador(es)</td>
<td></td>
<td></td>
</tr>
`);
} else {
const departamentoInfo = pidIndValorRepresentativo(colaboradoresDoDepartamento, "pct_departamento");
const criteriosDepartamento = renderMetaCriteriosHtml(
pidIndCriteriosDoGrupo("departamento", colaboradoresDoDepartamento)
);
linhas.push(
renderMetaLinhaHtml({
label: `Departamento (todo o ${grupo.nome})`,
criteriosHtml: criteriosDepartamento,
count: colaboradoresDoDepartamento.length,
valor: departamentoInfo.valor,
ajustado: departamentoInfo.ajustado,
inputAttrs: `data-ind-meta-tipo="departamento" data-ind-meta-departamento="${grupo.id}"`,
recalcularAttrs: `data-ind-meta-recalcular-departamento="${grupo.id}"`,
})
);
}
pidIndAgruparPorGerente(colaboradoresDoDepartamento).forEach(([gerente, colaboradoresDoGrupo]) => {
const grupoInfo = pidIndValorRepresentativo(colaboradoresDoGrupo, "pct_grupo");
const criteriosGrupo = renderMetaCriteriosHtml(pidIndCriteriosDoGrupo("grupo", colaboradoresDoGrupo));
linhas.push(
renderMetaLinhaHtml({
label: `Gerente: ${gerente || "Sem gerente definido"}`,
criteriosHtml: criteriosGrupo,
count: colaboradoresDoGrupo.length,
valor: grupoInfo.valor,
ajustado: grupoInfo.ajustado,
inputAttrs: `data-ind-meta-tipo="grupo" data-ind-meta-gerente="${pidIndEscapeAttr(gerente)}"`,
recalcularAttrs: `data-ind-meta-recalcular-grupo="${pidIndEscapeAttr(gerente)}"`,
})
);
});
});
body.innerHTML = linhas.join("");
}
// Meta de Grupo/Departamento só tem duas possibilidades de verdade — "será
// pago ou não" (decisão explícita do usuário) — por isso é Sim/Não (100%/0%),
// não um percentual livre como o "% Individual atingido" de cada colaborador.
function renderMetaLinhaHtml({ label, criteriosHtml, count, valor, ajustado, inputAttrs, recalcularAttrs }) {
const ehSim = Number(valor) >= 50;
return `
<tr>
<td>
<div class="ind-meta-label">${pidIndEscapeHtml(label)}</div>
${criteriosHtml}
</td>
<td>${count} colaborador(es)</td>
<td>
<select class="ind-meta-select" ${inputAttrs}>
<option value="100" ${ehSim ? "selected" : ""}>Sim</option>
<option value="0" ${ehSim ? "" : "selected"}>Não</option>
</select>
${ajustado ? '<span class="is-ajustado">ajustado manualmente</span>' : ""}
</td>
<td class="ind-meta-acao">
${
ajustado
? `<button type="button" class="ind-pct-reset-btn" ${recalcularAttrs} title="Recalcular automaticamente">↺</button>`
: ""
}
</td>
</tr>
`;
}
document.getElementById("ind-metas-body").addEventListener("change", async (event) => {
const select = event.target.closest(".ind-meta-select");
if (!select) return;
const errorEl = document.getElementById("ind-metas-error");
errorEl.textContent = "";
try {
if (select.getAttribute("data-ind-meta-tipo") === "departamento") {
await pidAjustarDepartamentoIndicador(
apuracaoAtual.id,
Number(select.getAttribute("data-ind-meta-departamento")),
select.value
);
} else {
await pidAjustarGrupoIndicador(apuracaoAtual.id, select.getAttribute("data-ind-meta-gerente"), select.value);
}
apuracaoAtual = await pidFetchApuracaoIndicador(apuracaoAtual.id);
renderRevisao();
} catch (e) {
errorEl.textContent = e.message;
}
});
document.getElementById("ind-metas-body").addEventListener("click", async (event) => {
const errorEl = document.getElementById("ind-metas-error");
const recalcularDepartamentoBtn = event.target.closest("[data-ind-meta-recalcular-departamento]");
if (recalcularDepartamentoBtn) {
errorEl.textContent = "";
try {
await pidRecalcularDepartamentoIndicador(
apuracaoAtual.id,
Number(recalcularDepartamentoBtn.getAttribute("data-ind-meta-recalcular-departamento"))
);
apuracaoAtual = await pidFetchApuracaoIndicador(apuracaoAtual.id);
renderRevisao();
} catch (e) {
errorEl.textContent = e.message;
}
return;
}
const recalcularGrupoBtn = event.target.closest("[data-ind-meta-recalcular-grupo]");
if (recalcularGrupoBtn) {
errorEl.textContent = "";
try {
await pidRecalcularGrupoIndicador(apuracaoAtual.id, recalcularGrupoBtn.getAttribute("data-ind-meta-recalcular-grupo"));
apuracaoAtual = await pidFetchApuracaoIndicador(apuracaoAtual.id);
renderRevisao();
} catch (e) {
errorEl.textContent = e.message;
}
}
});
// ---- Filtro por departamento (Fisco/Contábil, Rocket, Gerentes, ...) —
// afeta a tabela de Metas (renderMetasSection) e a lista de colaboradores
// abaixo ao mesmo tempo. ----
function renderFiltroDepartamento() {
const container = document.getElementById("ind-filtro-departamento");
const grupos = pidIndAgruparPorDepartamento(apuracaoAtual.colaboradores);
const chipHtml = (chave, rotulo) =>
`<button type="button" class="ind-departamento-chip ${filtroDepartamento === chave ? "is-active" : ""}" data-ind-filtro-departamento="${pidIndEscapeAttr(chave)}">${pidIndEscapeHtml(rotulo)}</button>`;
container.innerHTML =
chipHtml(PID_IND_FILTRO_TODOS, "Todos") + grupos.map((grupo) => chipHtml(grupo.chave, grupo.nome)).join("");
}
document.getElementById("ind-filtro-departamento").addEventListener("click", (event) => {
const chip = event.target.closest("[data-ind-filtro-departamento]");
if (!chip) return;
filtroDepartamento = chip.getAttribute("data-ind-filtro-departamento");
renderFiltroDepartamento();
renderMetasSection();
renderColaboradoresList();
});
function colaboradoresFiltrados() {
return filtroDepartamento === PID_IND_FILTRO_TODOS
? apuracaoAtual.colaboradores
: apuracaoAtual.colaboradores.filter(
(colaborador) => pidIndChaveDepartamento(colaborador.departamento) === filtroDepartamento
);
}
function renderColaboradoresList() {
const listEl = document.getElementById("ind-colaboradores-list");
listEl.innerHTML = "";
colaboradoresFiltrados().forEach((colaborador) => listEl.appendChild(renderColaboradorCard(colaborador)));
}
// ---- Empresas por código (popup "Empresas sem Honorário", com as duas seções) ----
// A mesma empresa pode aparecer sob mais de um colaborador — um responsável
// pelo balancete, outro pela liberação fiscal, outro pela conciliação
// financeira — mas o honorário é da empresa, não do colaborador. Agrupar
// por código deixa explícito que preencher (ou corrigir) uma vez já resolve
// todo mundo (aplicado via pidAjustarHonorarioEmpresaEmLoteIndicador, que
// atualiza todas as linhas com esse código de uma vez no backend — o mesmo
// endpoint cobre tanto o preenchimento inicial quanto a correção de um
// valor já ajustado, ver IndicadorApuracaoViewSet.ajustar_honorario_empresa).
function empresasAgrupadasPorCodigo(filtro) {
const porCodigo = new Map();
apuracaoAtual.colaboradores.forEach((colaborador) => {
colaborador.empresas.filter(filtro).forEach((empresa) => {
if (!porCodigo.has(empresa.codigo_empresa)) {
porCodigo.set(empresa.codigo_empresa, {
codigo_empresa: empresa.codigo_empresa,
nome_empresa: empresa.nome_empresa,
honorario: empresa.honorario,
responsaveis: [],
});
}
porCodigo.get(empresa.codigo_empresa).responsaveis.push({
nome: colaborador.nome,
tipoLabel: empresa.tipo_label,
empresaRowId: empresa.id,
colaboradorId: colaborador.id,
});
});
});
// Mesmo critério de IndicadorApuracaoEmpresa.Meta.ordering: tamanho da
// string primeiro — senão "80"/"503" ficam depois de "2134" (ordem
// alfabética, não numérica, quando os códigos têm tamanhos diferentes).
return Array.from(porCodigo.values()).sort((a, b) => {
const porTamanho = a.codigo_empresa.length - b.codigo_empresa.length;
return porTamanho !== 0 ? porTamanho : a.codigo_empresa.localeCompare(b.codigo_empresa);
});
}
function renderEmpresaGrupoItemHtml(grupo, valorInicial) {
return `
<div class="ind-esh-item">
<div class="ind-esh-header">
<span class="ind-esh-codigo">Código ${pidIndEscapeHtml(grupo.codigo_empresa)}</span>
<span class="ind-esh-nome">${pidIndEscapeHtml(grupo.nome_empresa || grupo.codigo_empresa)}</span>
</div>
<ul class="ind-esh-responsaveis">
${grupo.responsaveis
.map((r) => `<li>${pidIndEscapeHtml(r.nome)} — ${pidIndEscapeHtml(r.tipoLabel)}</li>`)
.join("")}
</ul>
<div class="ind-esh-form">
<input type="text" class="ind-empresa-honorario-input" placeholder="Informar honorário" value="${valorInicial}" />
<button type="button" class="btn-solid" data-ind-esh-salvar="${pidIndEscapeAttr(grupo.codigo_empresa)}">Salvar</button>
</div>
</div>`;
}
// Sem nada pendente, o botão deixa de ser um alerta ("sem honorário") e
// passa a apontar direto pra revisão do que já foi ajustado — pedido
// explícito do usuário: nada mais pra resolver, então nem faz sentido
// manter a cor de atenção (--danger).
function atualizarBotaoEmpresasSemHonorario() {
const btn = document.getElementById("ind-empresas-sem-honorario-btn");
const semHonorario = empresasAgrupadasPorCodigo((empresa) => empresa.honorario_nao_encontrado).length;
if (semHonorario > 0) {
btn.textContent = `Visualizar Empresas sem Honorário (${semHonorario})`;
btn.classList.add("ind-empresas-sem-honorario-btn");
return;
}
const ajustadas = empresasAgrupadasPorCodigo((empresa) => empresa.honorario_ajustado_manualmente).length;
btn.textContent = ajustadas
? `Visualizar Empresas com Honorário Ajustado (${ajustadas})`
: "Visualizar Empresas com Honorário Ajustado";
btn.classList.remove("ind-empresas-sem-honorario-btn");
}
function renderEmpresasSemHonorarioList() {
const grupos = empresasAgrupadasPorCodigo((empresa) => empresa.honorario_nao_encontrado);
document.getElementById("ind-empresas-sem-honorario-vazio").hidden = grupos.length > 0;
document.getElementById("ind-empresas-sem-honorario-list").innerHTML = grupos
.map((grupo) => renderEmpresaGrupoItemHtml(grupo, ""))
.join("");
}
// Diferente da lista de "sem honorário": aqui o campo já vem preenchido com
// o valor atual, porque o RH está corrigindo um número já informado, não
// preenchendo um em branco pela primeira vez. Fica escondida por padrão
// dentro do mesmo popup, atrás do botão "Visualizar Empresas Ajustadas
// Manualmente" (pedido explícito do usuário — no final do modal, revelada
// sob demanda, em vez de sempre visível junto com a lista principal).
function renderEmpresasAjustadasList() {
const grupos = empresasAgrupadasPorCodigo((empresa) => empresa.honorario_ajustado_manualmente);
document.getElementById("ind-empresas-ajustadas-vazio").hidden = grupos.length > 0;
document.getElementById("ind-empresas-ajustadas-list").innerHTML = grupos
.map((grupo) => renderEmpresaGrupoItemHtml(grupo, Number(grupo.honorario).toFixed(2)))
.join("");
}
function atualizarBotaoEmpresasAjustadas() {
const total = empresasAgrupadasPorCodigo((empresa) => empresa.honorario_ajustado_manualmente).length;
const aberta = !document.getElementById("ind-empresas-ajustadas-section").hidden;
const rotulo = aberta ? "Ocultar Empresas Ajustadas Manualmente" : "Visualizar Empresas Ajustadas Manualmente";
document.getElementById("ind-empresas-ajustadas-toggle-btn").textContent = total ? `${rotulo} (${total})` : rotulo;
}
// Reflete o estado atual nas duas seções — a "sem honorário" (sempre
// visível) e a "ajustadas" (só se já estiver aberta), usado tanto ao abrir
// o popup quanto depois de salvar um ajuste, já que corrigir uma empresa
// "sem honorário" faz ela migrar pra lista "ajustada manualmente" na hora.
function renderEmpresasHonorario() {
renderEmpresasSemHonorarioList();
if (!document.getElementById("ind-empresas-ajustadas-section").hidden) {
renderEmpresasAjustadasList();
}
atualizarBotaoEmpresasAjustadas();
}
async function salvarAjusteHonorarioEmpresa(event) {
const btn = event.target.closest("[data-ind-esh-salvar]");
if (!btn) return;
const errorEl = document.getElementById("ind-empresas-sem-honorario-error");
errorEl.textContent = "";
const input = btn.closest(".ind-esh-item").querySelector(".ind-empresa-honorario-input");
const valor = input.value.replace(",", ".").trim();
if (!valor || Number.isNaN(Number(valor))) {
errorEl.textContent = "Informe um honorário válido.";
return;
}
try {
await pidAjustarHonorarioEmpresaEmLoteIndicador(apuracaoAtual.id, btn.getAttribute("data-ind-esh-salvar"), valor);
apuracaoAtual = await pidFetchApuracaoIndicador(apuracaoAtual.id);
renderRevisao();
renderEmpresasHonorario();
} catch (e) {
errorEl.textContent = e.message;
}
}
document.getElementById("ind-empresas-sem-honorario-btn").addEventListener("click", () => {
document.getElementById("ind-empresas-sem-honorario-error").textContent = "";
document.getElementById("ind-empresas-ajustadas-section").hidden = true;
renderEmpresasHonorario();
document.getElementById("ind-empresas-sem-honorario-modal").hidden = false;
});
document.getElementById("ind-empresas-sem-honorario-close-btn").addEventListener("click", () => {
document.getElementById("ind-empresas-sem-honorario-modal").hidden = true;
});
document.getElementById("ind-empresas-sem-honorario-modal").addEventListener("click", (event) => {
if (event.target.id === "ind-empresas-sem-honorario-modal") {
document.getElementById("ind-empresas-sem-honorario-modal").hidden = true;
}
});
document.getElementById("ind-empresas-ajustadas-toggle-btn").addEventListener("click", () => {
const section = document.getElementById("ind-empresas-ajustadas-section");
section.hidden = !section.hidden;
if (!section.hidden) renderEmpresasAjustadasList();
atualizarBotaoEmpresasAjustadas();
});
document.getElementById("ind-empresas-sem-honorario-list").addEventListener("click", salvarAjusteHonorarioEmpresa);
document.getElementById("ind-empresas-ajustadas-list").addEventListener("click", salvarAjusteHonorarioEmpresa);
// ---- Corrigir Responsável (popup: busca uma empresa, mostra os
// responsáveis por cada tipo/papel dela e permite trocar quem responde) ----
let corrigirEmpresaSelecionada = null; // codigo_empresa da empresa selecionada na busca
function renderCorrigirEmpresaResultados(termo) {
const box = document.getElementById("ind-corrigir-empresa-resultados");
const termoNormalizado = termo.trim().toLowerCase();
if (!termoNormalizado) {
box.hidden = true;
box.innerHTML = "";
return;
}
const grupos = empresasAgrupadasPorCodigo(() => true).filter(
(grupo) =>
grupo.codigo_empresa.toLowerCase().includes(termoNormalizado) ||
(grupo.nome_empresa || "").toLowerCase().includes(termoNormalizado)
);
box.hidden = false;
box.innerHTML = grupos.length
? grupos
.map(
(grupo) => `
<button type="button" class="checklist-item ind-corrigir-resultado" data-ind-corrigir-empresa="${pidIndEscapeAttr(grupo.codigo_empresa)}">
<span class="ind-esh-codigo">Código ${pidIndEscapeHtml(grupo.codigo_empresa)}</span>
<span class="ind-esh-nome">${pidIndEscapeHtml(grupo.nome_empresa || grupo.codigo_empresa)}</span>
</button>`
)
.join("")
: '<p class="checklist-empty">Nenhuma empresa encontrada.</p>';
}
// Uma <select> por responsável, excluindo o próprio colaborador atual da
// lista de opções (trocar "pra ele mesmo" não faz sentido).
function renderCorrigirEmpresaDetalhe() {
const container = document.getElementById("ind-corrigir-empresa-detalhe");
const grupo = corrigirEmpresaSelecionada
? empresasAgrupadasPorCodigo(() => true).find((g) => g.codigo_empresa === corrigirEmpresaSelecionada)
: null;
if (!grupo) {
corrigirEmpresaSelecionada = null;
container.innerHTML = "";
return;
}
container.innerHTML = `
<div class="ind-esh-item">
<div class="ind-esh-header">
<span class="ind-esh-codigo">Código ${pidIndEscapeHtml(grupo.codigo_empresa)}</span>
<span class="ind-esh-nome">${pidIndEscapeHtml(grupo.nome_empresa || grupo.codigo_empresa)}</span>
</div>
<ul class="ind-esh-responsaveis ind-corrigir-responsaveis">
${grupo.responsaveis
.map((r) => {
const opcoes = apuracaoAtual.colaboradores
.filter((c) => c.id !== r.colaboradorId)
.map((c) => `<option value="${c.id}">${pidIndEscapeHtml(c.nome)}</option>`)
.join("");
return `
<li>
<span>${pidIndEscapeHtml(r.nome)} — ${pidIndEscapeHtml(r.tipoLabel)}</span>
<span class="ind-corrigir-responsavel-form">
<select data-ind-corrigir-empresa-row="${r.empresaRowId}">
<option value="" selected disabled>Selecionar novo responsável...</option>
${opcoes}
</select>
<button type="button" class="btn-solid" data-ind-corrigir-salvar="${r.empresaRowId}">Salvar</button>
</span>
</li>`;
})
.join("")}
</ul>
</div>
`;
}
document.getElementById("ind-corrigir-responsavel-btn").addEventListener("click", () => {
corrigirEmpresaSelecionada = null;
document.getElementById("ind-corrigir-empresa-search").value = "";
document.getElementById("ind-corrigir-empresa-resultados").hidden = true;
document.getElementById("ind-corrigir-empresa-resultados").innerHTML = "";
document.getElementById("ind-corrigir-empresa-detalhe").innerHTML = "";
document.getElementById("ind-corrigir-responsavel-error").textContent = "";
document.getElementById("ind-corrigir-responsavel-modal").hidden = false;
});
document.getElementById("ind-corrigir-responsavel-close-btn").addEventListener("click", () => {
document.getElementById("ind-corrigir-responsavel-modal").hidden = true;
});
document.getElementById("ind-corrigir-responsavel-modal").addEventListener("click", (event) => {
if (event.target.id === "ind-corrigir-responsavel-modal") {
document.getElementById("ind-corrigir-responsavel-modal").hidden = true;
}
});
document.getElementById("ind-corrigir-empresa-search").addEventListener("input", (event) => {
renderCorrigirEmpresaResultados(event.target.value);
});
document.getElementById("ind-corrigir-empresa-resultados").addEventListener("click", (event) => {
const btn = event.target.closest("[data-ind-corrigir-empresa]");
if (!btn) return;
corrigirEmpresaSelecionada = btn.getAttribute("data-ind-corrigir-empresa");
document.getElementById("ind-corrigir-empresa-search").value = "";
document.getElementById("ind-corrigir-empresa-resultados").hidden = true;
document.getElementById("ind-corrigir-empresa-resultados").innerHTML = "";
renderCorrigirEmpresaDetalhe();
});
document.getElementById("ind-corrigir-empresa-detalhe").addEventListener("click", async (event) => {
const btn = event.target.closest("[data-ind-corrigir-salvar]");
if (!btn) return;
const errorEl = document.getElementById("ind-corrigir-responsavel-error");
errorEl.textContent = "";
const empresaRowId = Number(btn.getAttribute("data-ind-corrigir-salvar"));
const select = document.querySelector(`select[data-ind-corrigir-empresa-row="${empresaRowId}"]`);
const novoColaboradorId = Number(select.value);
if (!novoColaboradorId) {
errorEl.textContent = "Selecione um colaborador.";
return;
}
try {
await pidTrocarResponsavelEmpresaIndicador(empresaRowId, novoColaboradorId);
apuracaoAtual = await pidFetchApuracaoIndicador(apuracaoAtual.id);
renderRevisao();
renderCorrigirEmpresaDetalhe();
} catch (e) {
errorEl.textContent = e.message;
}
});
function renderPctResumoHtml({ label, valor, ajustado, editarAttrs, recalcularAttrs, corClasse }) {
return `
<span class="ind-pct-field ind-pct-field--resumo">
<span class="ind-pct-field__principal">
${label}: <span class="ind-pct-value ${corClasse || ""}">${pidIndFormatPercentual(valor)}</span>
<button type="button" class="ind-pct-edit-btn" ${editarAttrs} title="Ajustar manualmente">✎</button>
</span>
${
ajustado
? `<span class="is-ajustado">ajustado manualmente</span>
<button type="button" class="ind-pct-reset-btn" ${recalcularAttrs} title="Recalcular automaticamente">↺</button>`
: ""
}
</span>
`;
}
// "Individual (peso 60%) + Grupo (peso 10%) + Departamento (peso 30%) =
// Total Indicador" — a composição que CLAUDE.md documenta (ver
// portal_api.indicadores.calculo.composicao_individual), montada no backend
// pra não duplicar a lógica de arredondamento/composição em JS. Cada nível
// fica verde/vermelho conforme foi atingido (100%) ou não — só o Total
// (renderPctResumoHtml) fica neutro, pra não repetir a mesma cor 4 vezes.
function renderComposicaoIndicadorHtml(composicao) {
const niveis = [
{ label: "Individual", dados: composicao.individual },
{ label: "Grupo", dados: composicao.grupo },
{ label: "Departamento", dados: composicao.departamento },
]
.map(({ label, dados }) => {
const corClasse = pidIndPercentualAtingido(dados.percentual) ? "is-atingido" : "is-nao-atingido";
return `${label}: <b class="${corClasse}">${pidIndFormatPercentual(dados.percentual)}</b> (peso ${pidIndFormatPeso(dados.peso)})`;
})
.join(" &middot; ");
return `<div class="ind-colaborador-card__composicao">${niveis}</div>`;
}
// Botão "i" ao lado do cabeçalho "Honorário Ajustado" — explica de onde
// vem esse valor (Honorário × % do Total Indicador do colaborador, ver
// calculo.py) mostrando já o percentual real usado nesta apuração, não só
// um texto genérico. Popover próprio (não reaproveita nenhum modal
// existente) — clique fora fecha, mesmo padrão de `.notif-dropdown`.
function renderHonorarioAjustadoInfoHtml(colaborador) {
const pct = pidIndFormatPercentual(colaborador.pct_individual);
return `
<span class="ind-info-wrap">
<button type="button" class="ind-info-btn" data-ind-info-toggle aria-label="O que é o Honorário Ajustado?" title="O que é o Honorário Ajustado?">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-5" stroke-linecap="round"/><path d="M12 8h.01" stroke-linecap="round"/></svg>
</button>
<span class="ind-info-popover" hidden>Honorário já proporcionalizado ao percentual do Total Indicador atingido pelo colaborador — nesse caso, ${pct}.</span>
</span>
`;
}
function renderColaboradorCard(colaborador) {
const card = document.createElement("div");
card.className = colaborador.validado ? "ind-colaborador-card is-validado" : "ind-colaborador-card";
const composicao = colaborador.composicao_individual;
const header = document.createElement("div");
header.className = "ind-colaborador-card__header";
header.innerHTML = `
<label class="ind-colaborador-card__validado" title="Marcar como validado">
<input type="checkbox" data-ind-colaborador-validado="${colaborador.id}" ${colaborador.validado ? "checked" : ""} />
</label>
<div>
<div class="ind-colaborador-card__nome">${pidIndEscapeHtml(colaborador.nome)}</div>
<div class="ind-colaborador-card__gerente">Gerente: ${pidIndEscapeHtml(colaborador.gerente || "Sem gerente definido")}</div>
</div>
${composicao ? renderComposicaoIndicadorHtml(composicao) : "<div></div>"}
<div class="ind-colaborador-card__pcts">
${renderPctResumoHtml({
label: "Total Indicador",
valor: colaborador.pct_individual,
ajustado: colaborador.pct_individual_ajustado_manualmente,
editarAttrs: `data-ind-pct-editar-individual="${colaborador.id}"`,
recalcularAttrs: `data-ind-pct-recalcular-individual="${colaborador.id}"`,
})}
</div>
<div class="ind-colaborador-card__total">${pidIndFormatMoeda(colaborador.valor_total)}</div>
`;
header.addEventListener("click", (event) => {
// .ind-colaborador-card__validado (o <label>, não só o <input>) — um
// clique na área do label fora do glifo do checkbox ainda dispara um
// evento de clique com target=label antes do clique sintético no
// input; sem excluir o label inteiro, esse primeiro clique passava
// pelo filtro (closest só olha pros data-attrs do <input>) e expandia/
// recolhia o card ao mesmo tempo que marcava/desmarcava o validado.
if (
event.target.closest(
"[data-ind-pct-editar-individual], [data-ind-pct-recalcular-individual], .ind-colaborador-card__validado"
)
)
return;
body.hidden = !body.hidden;
});
const body = document.createElement("div");
body.className = "ind-colaborador-card__body";
body.hidden = true;
body.innerHTML = `
<p class="ind-section-title">Empresas</p>
<div class="pa-table-wrap">
<table class="pa-table ind-empresas-table">
<thead><tr><th>Código</th><th>Empresa</th><th>Honorário</th><th>Honorário Ajustado ${renderHonorarioAjustadoInfoHtml(colaborador)}</th><th>Tipo</th><th>Individual</th><th>Grupo</th><th>Departamento</th><th>Total</th></tr></thead>
<tbody>
${colaborador.empresas
.map(
(empresa) => `
<tr>
<td>${pidIndEscapeHtml(empresa.codigo_empresa)}</td>
<td>${pidIndEscapeHtml(empresa.nome_empresa || empresa.codigo_empresa)}${empresa.honorario_nao_encontrado ? ' <span class="status-pill status-pill--inativo">honorário não encontrado</span>' : ""}</td>
<td>
${
empresa.honorario_nao_encontrado
? `<input type="text" class="ind-empresa-honorario-input" data-ind-empresa-honorario="${empresa.id}" placeholder="Informar honorário" />`
: `${pidIndFormatMoeda(empresa.honorario)}${
empresa.honorario_ajustado_manualmente
? ' <span class="is-ajustado">honorário ajustado manualmente</span>'
: ""
}`
}
</td>
<td>${pidIndFormatMoeda(empresa.honorario_ajustado)}</td>
<td>${pidIndEscapeHtml(empresa.tipo_label)}</td>
<td>${pidIndFormatMoeda(empresa.valor_individual)}</td>
<td>${pidIndFormatMoeda(empresa.valor_grupo)}</td>
<td>${pidIndFormatMoeda(empresa.valor_departamento)}</td>
<td>${pidIndFormatMoeda(empresa.valor_total)}</td>
</tr>`
)
.join("")}
</tbody>
</table>
</div>
${["individual", "grupo", "departamento"]
.map((grupo) => renderRespostasGrupoHtml(colaborador, grupo))
.join("")}
`;
card.appendChild(header);
card.appendChild(body);
return card;
}
// Grupo/Departamento não têm "não se aplica"/"não faz" real — o
// colaborador sempre faz parte de um grupo e do departamento (decisão
// explícita do usuário, ver CLAUDE.md) — então o select ali só oferece
// Sim/Não. Individual continua com as 4 opções (pode genuinamente não se
// aplicar ao papel do colaborador).
function pidIndOpcoesRespostaHtml(valorAtual, grupo) {
if (grupo === "individual") {
return `
<option value="SIM" ${valorAtual === "SIM" ? "selected" : ""}>Sim</option>
<option value="NAO" ${valorAtual === "NAO" ? "selected" : ""}>Não</option>
<option value="NAO_FAZ" ${valorAtual === "NAO_FAZ" ? "selected" : ""}>Não faz</option>
<option value="NAO_SE_APLICA" ${valorAtual === "NAO_SE_APLICA" ? "selected" : ""}>Não se aplica</option>
`;
}
return `
<option value="SIM" ${valorAtual === "SIM" ? "selected" : ""}>Sim</option>
<option value="NAO" ${valorAtual === "NAO" ? "selected" : ""}>Não</option>
`;
}
function renderRespostasGrupoHtml(colaborador, grupo) {
const respostas = pidIndRespostasDoGrupo(colaborador, grupo);
if (!respostas.length) return "";
const periodoAtual = pidIndPeriodoAtual(apuracaoAtual.competencia);
return `
<p class="ind-section-title">${PID_IND_GRUPO_LABELS[grupo]}</p>
<div class="pa-table-wrap">
<table class="pa-table ind-respostas-table">
<thead><tr><th class="ind-select-col"></th><th>Critério</th><th>Peso</th><th>% Atingido</th><th>Valor</th></tr></thead>
<tbody>
${respostas
.map((resposta) => {
const naoUtilizada = pidIndRespostaNaoUtilizada(resposta, colaborador, periodoAtual);
return `
<tr class="${naoUtilizada ? "is-apagado" : ""}">
<td class="ind-select-col"><input type="checkbox" data-ind-resposta-check="${resposta.id}" /></td>
<td>${pidIndEscapeHtml(resposta.criterio_nome)}${naoUtilizada ? ' <span class="ind-apagado-badge">apagado — não entra no cálculo</span>' : ""}</td>
<td>${pidIndFormatPercentual(resposta.criterio_peso)}</td>
<td>${resposta.percentual_calculado != null ? pidIndFormatPercentual(resposta.percentual_calculado) : "—"}</td>
<td>
<select data-ind-resposta-select="${resposta.id}">
${pidIndOpcoesRespostaHtml(resposta.valor, grupo)}
</select>
${resposta.ajustado_manualmente ? '<span class="is-ajustado">ajustado manualmente</span>' : ""}
</td>
</tr>`;
})
.join("")}
</tbody>
</table>
</div>
`;
}
function atualizarBarraLote() {
const bar = document.getElementById("ind-lote-bar");
bar.hidden = respostasSelecionadas.size === 0;
document.getElementById("ind-lote-count").textContent = `${respostasSelecionadas.size} selecionada(s)`;
}
// Preencher aqui o honorário de uma empresa com "honorário não encontrado"
// (código sem casamento na planilha de Honorários Por Cliente) zera esse
// flag e recalcula o colaborador (honorário ajustado, valores individual/
// grupo/departamento — ver IndicadorApuracaoEmpresaViewSet).
document.getElementById("ind-colaboradores-list").addEventListener("focusout", async (event) => {
const input = event.target.closest(".ind-empresa-honorario-input");
if (!input) return;
const errorEl = document.getElementById("ind-review-error");
errorEl.textContent = "";
const raw = input.value.replace(",", ".").trim();
if (!raw) return;
if (Number.isNaN(Number(raw))) {
errorEl.textContent = "Informe um honorário válido.";
return;
}
const empresaId = Number(input.getAttribute("data-ind-empresa-honorario"));
try {
await pidAjustarHonorarioEmpresaIndicador(empresaId, raw);
apuracaoAtual = await pidFetchApuracaoIndicador(apuracaoAtual.id);
renderRevisao();
} catch (e) {
errorEl.textContent = e.message;
}
});
document.getElementById("ind-colaboradores-list").addEventListener("change", async (event) => {
// Checklist de revisão do RH — atualiza só o card clicado (sem recarregar
// a apuração inteira, ao contrário dos outros ajustes desta tela), pra
// não fechar outros cards já expandidos nem perder a posição da rolagem
// no meio de uma conferência com muitos colaboradores. Erro de rede
// reverte o checkbox e o estado em memória, mesmo padrão de outros
// toggles imediatos do app (ex. inativar usuário).
const validadoCheckbox = event.target.closest("[data-ind-colaborador-validado]");
if (validadoCheckbox) {
const id = Number(validadoCheckbox.getAttribute("data-ind-colaborador-validado"));
const novoValor = validadoCheckbox.checked;
const card = validadoCheckbox.closest(".ind-colaborador-card");
card.classList.toggle("is-validado", novoValor);
try {
await pidMarcarValidadoColaboradorIndicador(id, novoValor);
const colaborador = apuracaoAtual.colaboradores.find((c) => c.id === id);
if (colaborador) colaborador.validado = novoValor;
} catch (e) {
validadoCheckbox.checked = !novoValor;
card.classList.toggle("is-validado", !novoValor);
alert(e.message);
}
return;
}
const checkbox = event.target.closest("[data-ind-resposta-check]");
if (checkbox) {
const id = Number(checkbox.getAttribute("data-ind-resposta-check"));
if (checkbox.checked) respostasSelecionadas.add(id);
else respostasSelecionadas.delete(id);
atualizarBarraLote();
return;
}
const select = event.target.closest("[data-ind-resposta-select]");
if (select) {
const id = Number(select.getAttribute("data-ind-resposta-select"));
const errorEl = document.getElementById("ind-review-error");
errorEl.textContent = "";
try {
await pidAtualizarRespostaIndicador(id, select.value);
apuracaoAtual = await pidFetchApuracaoIndicador(apuracaoAtual.id);
renderRevisao();
} catch (e) {
errorEl.textContent = e.message;
}
}
});
// Grupo/Departamento agora são preenchidos na tabela de metas no topo da
// página (ver renderMetasSection) — este modal genérico só cobre o
// percentual Individual, que continua por colaborador.
let ajusteColaboradorId = null;
const ajustePctModal = document.getElementById("ind-ajuste-pct-modal");
document.getElementById("ind-colaboradores-list").addEventListener("click", async (event) => {
const infoToggleBtn = event.target.closest("[data-ind-info-toggle]");
if (infoToggleBtn) {
const popover = infoToggleBtn.nextElementSibling;
const jaAberto = !popover.hidden;
document
.querySelectorAll("#ind-colaboradores-list .ind-info-popover")
.forEach((outro) => (outro.hidden = true));
popover.hidden = jaAberto;
return;
}
const editarIndividualBtn = event.target.closest("[data-ind-pct-editar-individual]");
if (editarIndividualBtn) {
ajusteColaboradorId = Number(editarIndividualBtn.getAttribute("data-ind-pct-editar-individual"));
const colaborador = apuracaoAtual.colaboradores.find((c) => c.id === ajusteColaboradorId);
document.getElementById("ind-ajuste-pct-titulo").textContent = `Ajustar percentual — Total Indicador (${colaborador.nome})`;
document.getElementById("ind-ajuste-pct-subtitle").textContent =
'O valor informado passa a valer só pra esse colaborador, até você reverter (botão "Recalcular automaticamente").';
document.getElementById("ind-ajuste-pct-valor").value = Number(colaborador.pct_individual).toFixed(2);
document.getElementById("ind-ajuste-pct-error").textContent = "";
ajustePctModal.hidden = false;
return;
}
const recalcularIndividualBtn = event.target.closest("[data-ind-pct-recalcular-individual]");
if (recalcularIndividualBtn) {
const colaboradorId = Number(recalcularIndividualBtn.getAttribute("data-ind-pct-recalcular-individual"));
const errorEl = document.getElementById("ind-review-error");
errorEl.textContent = "";
try {
await pidRecalcularColaboradorIndicador(colaboradorId);
apuracaoAtual = await pidFetchApuracaoIndicador(apuracaoAtual.id);
renderRevisao();
} catch (e) {
errorEl.textContent = e.message;
}
}
});
// Fecha o popover de "Honorário Ajustado" ao clicar fora dele — mesmo
// padrão de fechamento de `.notif-dropdown` (notifications.js).
document.addEventListener("click", (event) => {
if (event.target.closest(".ind-info-wrap")) return;
document.querySelectorAll(".ind-info-popover").forEach((popover) => {
popover.hidden = true;
});
});
document.getElementById("ind-ajuste-pct-cancel-btn").addEventListener("click", () => {
ajustePctModal.hidden = true;
});
ajustePctModal.addEventListener("click", (event) => {
if (event.target === ajustePctModal) ajustePctModal.hidden = true;
});
document.getElementById("ind-ajuste-pct-save-btn").addEventListener("click", async () => {
const errorEl = document.getElementById("ind-ajuste-pct-error");
errorEl.textContent = "";
const valor = document.getElementById("ind-ajuste-pct-valor").value.replace(",", ".");
if (!valor || Number.isNaN(Number(valor))) {
errorEl.textContent = "Informe um percentual válido.";
return;
}
try {
await pidAjustarPercentualColaboradorIndicador(ajusteColaboradorId, "pct_individual", valor);
ajustePctModal.hidden = true;
apuracaoAtual = await pidFetchApuracaoIndicador(apuracaoAtual.id);
renderRevisao();
} catch (e) {
errorEl.textContent = e.message;
}
});
document.getElementById("ind-lote-aplicar-btn").addEventListener("click", async () => {
const errorEl = document.getElementById("ind-review-error");
errorEl.textContent = "";
const valor = document.getElementById("ind-lote-valor").value;
try {
await pidAplicarEmLoteIndicador(Array.from(respostasSelecionadas), valor);
respostasSelecionadas.clear();
apuracaoAtual = await pidFetchApuracaoIndicador(apuracaoAtual.id);
renderRevisao();
} catch (e) {
errorEl.textContent = e.message;
}
});
document.getElementById("ind-review-back-btn").addEventListener("click", async () => {
showView("list");
await carregarLista();
});
// ---- Ajuste Indicador em Lote (percentual Individual, entre colaboradores selecionados) ----
const loteGlobalModal = document.getElementById("ind-lote-global-modal");
const loteGlobalSelecionados = new Set();
let loteGlobalBusca = "";
let loteGlobalDepartamento = PID_IND_FILTRO_TODOS;
function loteGlobalColaboradoresFiltrados() {
const termo = loteGlobalBusca.trim().toLowerCase();
return apuracaoAtual.colaboradores.filter((colaborador) => {
if (termo && !colaborador.nome.toLowerCase().includes(termo)) return false;
if (
loteGlobalDepartamento !== PID_IND_FILTRO_TODOS &&
pidIndChaveDepartamento(colaborador.departamento) !== loteGlobalDepartamento
)
return false;
return true;
});
}
function renderLoteGlobalDepartamentoOptions() {
const select = document.getElementById("ind-lote-global-departamento");
const grupos = pidIndAgruparPorDepartamento(apuracaoAtual.colaboradores);
select.innerHTML =
`<option value="${PID_IND_FILTRO_TODOS}">Todos os departamentos</option>` +
grupos.map((grupo) => `<option value="${pidIndEscapeAttr(grupo.chave)}">${pidIndEscapeHtml(grupo.nome)}</option>`).join("");
select.value = loteGlobalDepartamento;
}
function renderLoteGlobalChecklist() {
const filtrados = loteGlobalColaboradoresFiltrados();
const box = document.getElementById("ind-lote-global-checklist");
box.innerHTML = filtrados.length
? filtrados
.map(
(colaborador) => `
<label class="checklist-item">
<input type="checkbox" data-ind-lote-global-colaborador="${colaborador.id}" ${loteGlobalSelecionados.has(colaborador.id) ? "checked" : ""} />
<span class="checklist-item__nome">${pidIndEscapeHtml(colaborador.nome)}</span>
</label>`
)
.join("")
: '<p class="checklist-empty">Nenhum colaborador encontrado.</p>';
const selectAll = document.getElementById("ind-lote-global-select-all");
const todosMarcados = filtrados.length > 0 && filtrados.every((colaborador) => loteGlobalSelecionados.has(colaborador.id));
const algunsMarcados = filtrados.some((colaborador) => loteGlobalSelecionados.has(colaborador.id));
selectAll.checked = todosMarcados;
selectAll.indeterminate = !todosMarcados && algunsMarcados;
selectAll.disabled = filtrados.length === 0;
}
document.getElementById("ind-lote-global-btn").addEventListener("click", () => {
loteGlobalSelecionados.clear();
loteGlobalBusca = "";
loteGlobalDepartamento = PID_IND_FILTRO_TODOS;
document.getElementById("ind-lote-global-search").value = "";
document.getElementById("ind-lote-global-error").textContent = "";
renderLoteGlobalDepartamentoOptions();
renderLoteGlobalChecklist();
loteGlobalModal.hidden = false;
});
document.getElementById("ind-lote-global-cancel-btn").addEventListener("click", () => {
loteGlobalModal.hidden = true;
});
loteGlobalModal.addEventListener("click", (event) => {
if (event.target === loteGlobalModal) loteGlobalModal.hidden = true;
});
document.getElementById("ind-lote-global-search").addEventListener("input", (event) => {
loteGlobalBusca = event.target.value;
renderLoteGlobalChecklist();
});
document.getElementById("ind-lote-global-departamento").addEventListener("change", (event) => {
loteGlobalDepartamento = event.target.value;
renderLoteGlobalChecklist();
});
document.getElementById("ind-lote-global-select-all").addEventListener("change", (event) => {
const filtrados = loteGlobalColaboradoresFiltrados();
if (event.target.checked) filtrados.forEach((colaborador) => loteGlobalSelecionados.add(colaborador.id));
else filtrados.forEach((colaborador) => loteGlobalSelecionados.delete(colaborador.id));
renderLoteGlobalChecklist();
});
document.getElementById("ind-lote-global-checklist").addEventListener("change", (event) => {
const checkbox = event.target.closest("[data-ind-lote-global-colaborador]");
if (!checkbox) return;
const id = Number(checkbox.getAttribute("data-ind-lote-global-colaborador"));
if (checkbox.checked) loteGlobalSelecionados.add(id);
else loteGlobalSelecionados.delete(id);
renderLoteGlobalChecklist();
});
// Grupo/Departamento são ajustados na tela de Metas (no início da
// revisão) — estes botões cobrem só o Indicador Individual, aplicando de
// uma vez a todos os colaboradores selecionados: "Sim" considera o
// indicador atingido (100%, ex.: a diretoria decide pagar integral pra
// quem não bateu a meta individual); "Reverter" desfaz qualquer ajuste
// manual anterior e volta pro cálculo automático. Reaproveita os mesmos
// endpoints por-colaborador do ajuste individual (não existe ajuste em
// lote dedicado pra isso no backend), disparando um PATCH/POST por
// colaborador em paralelo.
async function aplicarLoteGlobal(acao) {
const errorEl = document.getElementById("ind-lote-global-error");
errorEl.textContent = "";
if (loteGlobalSelecionados.size === 0) {
errorEl.textContent = "Selecione ao menos um colaborador.";
return;
}
try {
await Promise.all(
Array.from(loteGlobalSelecionados).map((colaboradorId) =>
acao === "sim"
? pidAjustarPercentualColaboradorIndicador(colaboradorId, "pct_individual", "100")
: pidRecalcularColaboradorIndicador(colaboradorId)
)
);
loteGlobalModal.hidden = true;
apuracaoAtual = await pidFetchApuracaoIndicador(apuracaoAtual.id);
renderRevisao();
} catch (e) {
errorEl.textContent = e.message;
}
}
document.getElementById("ind-lote-global-ajustar-btn").addEventListener("click", () => aplicarLoteGlobal("sim"));
document.getElementById("ind-lote-global-reverter-btn").addEventListener("click", () => aplicarLoteGlobal("reverter"));
// ---- Gerar Recibos (modal: escolher quem recebe recibo desta vez — um
// colaborador só, alguns específicos, por departamento ou todos) ----
// Mesmo componente de busca+filtro por departamento+checklist do "Ajuste
// Indicador em Lote" acima, só que nasce com todo mundo já marcado (pra
// reproduzir o comportamento antigo — gerar pra todos — sem precisar
// marcar um por um; o RH desmarca quem não quer incluir desta vez).
const gerarRecibosModal = document.getElementById("ind-gerar-recibos-modal");
const gerarRecibosSelecionados = new Set();
let gerarRecibosBusca = "";
let gerarRecibosDepartamento = PID_IND_FILTRO_TODOS;
function gerarRecibosColaboradoresFiltrados() {
const termo = gerarRecibosBusca.trim().toLowerCase();
return apuracaoAtual.colaboradores.filter((colaborador) => {
if (termo && !colaborador.nome.toLowerCase().includes(termo)) return false;
if (
gerarRecibosDepartamento !== PID_IND_FILTRO_TODOS &&
pidIndChaveDepartamento(colaborador.departamento) !== gerarRecibosDepartamento
)
return false;
return true;
});
}
function renderGerarRecibosDepartamentoOptions() {
const select = document.getElementById("ind-gerar-recibos-departamento");
const grupos = pidIndAgruparPorDepartamento(apuracaoAtual.colaboradores);
select.innerHTML =
`<option value="${PID_IND_FILTRO_TODOS}">Todos os departamentos</option>` +
grupos.map((grupo) => `<option value="${pidIndEscapeAttr(grupo.chave)}">${pidIndEscapeHtml(grupo.nome)}</option>`).join("");
select.value = gerarRecibosDepartamento;
}
function renderGerarRecibosChecklist() {
const filtrados = gerarRecibosColaboradoresFiltrados();
const box = document.getElementById("ind-gerar-recibos-checklist");
box.innerHTML = filtrados.length
? filtrados
.map(
(colaborador) => `
<label class="checklist-item">
<input type="checkbox" data-ind-gerar-recibos-colaborador="${colaborador.id}" ${gerarRecibosSelecionados.has(colaborador.id) ? "checked" : ""} />
<span class="checklist-item__nome">${pidIndEscapeHtml(colaborador.nome)}</span>
</label>`
)
.join("")
: '<p class="checklist-empty">Nenhum colaborador encontrado.</p>';
const selectAll = document.getElementById("ind-gerar-recibos-select-all");
const todosMarcados = filtrados.length > 0 && filtrados.every((colaborador) => gerarRecibosSelecionados.has(colaborador.id));
const algunsMarcados = filtrados.some((colaborador) => gerarRecibosSelecionados.has(colaborador.id));
selectAll.checked = todosMarcados;
selectAll.indeterminate = !todosMarcados && algunsMarcados;
selectAll.disabled = filtrados.length === 0;
}
document.getElementById("ind-gerar-btn").addEventListener("click", () => {
gerarRecibosBusca = "";
gerarRecibosDepartamento = PID_IND_FILTRO_TODOS;
document.getElementById("ind-gerar-recibos-search").value = "";
document.getElementById("ind-gerar-recibos-error").textContent = "";
gerarRecibosSelecionados.clear();
apuracaoAtual.colaboradores.forEach((colaborador) => gerarRecibosSelecionados.add(colaborador.id));
renderGerarRecibosDepartamentoOptions();
renderGerarRecibosChecklist();
gerarRecibosModal.hidden = false;
});
document.getElementById("ind-gerar-recibos-cancel-btn").addEventListener("click", () => {
gerarRecibosModal.hidden = true;
});
gerarRecibosModal.addEventListener("click", (event) => {
if (event.target === gerarRecibosModal) gerarRecibosModal.hidden = true;
});
document.getElementById("ind-gerar-recibos-search").addEventListener("input", (event) => {
gerarRecibosBusca = event.target.value;
renderGerarRecibosChecklist();
});
document.getElementById("ind-gerar-recibos-departamento").addEventListener("change", (event) => {
gerarRecibosDepartamento = event.target.value;
renderGerarRecibosChecklist();
});
document.getElementById("ind-gerar-recibos-select-all").addEventListener("change", (event) => {
const filtrados = gerarRecibosColaboradoresFiltrados();
if (event.target.checked) filtrados.forEach((colaborador) => gerarRecibosSelecionados.add(colaborador.id));
else filtrados.forEach((colaborador) => gerarRecibosSelecionados.delete(colaborador.id));
renderGerarRecibosChecklist();
});
document.getElementById("ind-gerar-recibos-checklist").addEventListener("change", (event) => {
const checkbox = event.target.closest("[data-ind-gerar-recibos-colaborador]");
if (!checkbox) return;
const id = Number(checkbox.getAttribute("data-ind-gerar-recibos-colaborador"));
if (checkbox.checked) gerarRecibosSelecionados.add(id);
else gerarRecibosSelecionados.delete(id);
renderGerarRecibosChecklist();
});
document.getElementById("ind-gerar-recibos-confirm-btn").addEventListener("click", async () => {
const errorEl = document.getElementById("ind-gerar-recibos-error");
errorEl.textContent = "";
if (gerarRecibosSelecionados.size === 0) {
errorEl.textContent = "Selecione ao menos um colaborador.";
return;
}
try {
await pidGerarRecibosIndicador(apuracaoAtual.id, Array.from(gerarRecibosSelecionados));
gerarRecibosModal.hidden = true;
apuracaoAtual = await pidFetchApuracaoIndicador(apuracaoAtual.id);
renderRevisao();
await carregarLista();
} catch (e) {
errorEl.textContent = e.message;
}
});
// ---- Configurações ----
document.getElementById("ind-config-btn").addEventListener("click", async () => {
await carregarConfig();
showView("config");
});
document.getElementById("ind-config-back-btn").addEventListener("click", () => showView("list"));
document.getElementById("ind-config-tabs").addEventListener("click", (event) => {
const tabBtn = event.target.closest("[data-ind-config-tab]");
if (!tabBtn) return;
const tab = tabBtn.getAttribute("data-ind-config-tab");
document.querySelectorAll("#ind-config-tabs .pa-tab").forEach((btn) => btn.classList.toggle("is-active", btn === tabBtn));
document.querySelectorAll("[data-ind-config-panel]").forEach((panel) => {
panel.hidden = panel.getAttribute("data-ind-config-panel") !== tab;
});
});
let criterioFiltroDepartamento = PID_IND_FILTRO_TODOS;
let percentualFiltroDepartamento = PID_IND_FILTRO_TODOS;
let gerentesSugeridos = [];
// Nomes de `gerente` distintos da apuração mais recente (a lista já vem
// ordenada por `-competencia`/`-criado_em`, ver IndicadorApuracao.Meta) que
// ainda não estão em nenhum IndicadorDepartamentoGerente — poupa o RH de
// digitar os nomes de novo (arriscando um typo que faria a apuração futura
// não casar com o departamento certo) na hora de montar a relação
// gerente→departamento pela primeira vez.
async function carregarGerentesSugeridos() {
try {
const apuracoes = await pidFetchApuracoesIndicador();
if (!apuracoes.length) {
gerentesSugeridos = [];
return;
}
const ultima = await pidFetchApuracaoIndicador(apuracoes[0].id);
const nomesJaMapeados = new Set(departamentoGerentesConfig.map((g) => g.nome_gerente));
const distintos = new Set(
ultima.colaboradores.map((c) => c.gerente).filter((nome) => nome && !nomesJaMapeados.has(nome))
);
gerentesSugeridos = Array.from(distintos).sort((a, b) => a.localeCompare(b, "pt-BR"));
} catch (e) {
gerentesSugeridos = [];
}
}
async function carregarConfig() {
[criterios, percentuais, departamentosConfig, departamentoGerentesConfig] = await Promise.all([
pidFetchCriteriosIndicador(),
pidFetchPercentuaisIndicador(),
pidFetchDepartamentosIndicador(),
pidFetchDepartamentoGerentesIndicador(),
]);
await carregarGerentesSugeridos();
criterioFiltroDepartamento = PID_IND_FILTRO_TODOS;
percentualFiltroDepartamento = PID_IND_FILTRO_TODOS;
renderDepartamentoFiltroOptions("ind-criterio-departamento-filtro");
renderDepartamentoFiltroOptions("ind-percentual-departamento-filtro");
renderCriterios();
renderPercentuais();
renderDepartamentosConfig();
}
// Reaproveitado pelos dois filtros (Critérios/Percentuais) — lista de
// departamentos cadastrados, com "Todos" no topo.
function renderDepartamentoFiltroOptions(selectId) {
const select = document.getElementById(selectId);
select.innerHTML =
`<option value="${PID_IND_FILTRO_TODOS}">Todos os departamentos</option>` +
departamentosConfig.map((d) => `<option value="${d.id}">${pidIndEscapeHtml(d.nome)}</option>`).join("");
select.value = PID_IND_FILTRO_TODOS;
}
function renderCriterios() {
const body = document.getElementById("ind-criterios-body");
const filtrados =
criterioFiltroDepartamento === PID_IND_FILTRO_TODOS
? criterios
: criterios.filter((c) => c.departamento === criterioFiltroDepartamento);
body.innerHTML = filtrados
.map(
(criterio) => `
<tr>
<td>${pidIndEscapeHtml(criterio.nome)}</td>
<td>${pidIndEscapeHtml(criterio.departamento_nome)}</td>
<td>${PID_IND_GRUPO_LABELS[criterio.grupo] || criterio.grupo}</td>
<td>${pidIndFormatPercentual(criterio.peso)}</td>
<td>${PID_IND_PERIODO_LABELS[criterio.periodo] || criterio.periodo}</td>
<td>${criterio.papel_aplicavel ? PID_IND_TIPO_LABELS[criterio.papel_aplicavel] : "Qualquer"}</td>
<td>${PID_IND_CALCULO_LABELS[criterio.calculo_automatico] || criterio.calculo_automatico}</td>
<td><span class="status-pill ${criterio.ativo ? "status-pill--ativo" : "status-pill--inativo"}">${criterio.ativo ? "Ativo" : "Inativo"}</span></td>
<td class="pa-row-actions">
<button type="button" class="icon-btn" data-ind-criterio-toggle="${criterio.id}" title="${criterio.ativo ? "Desativar" : "Ativar"}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v10" stroke-linecap="round"/><path d="M6 6a8 8 0 1 0 12 0" stroke-linecap="round"/></svg>
</button>
<button type="button" class="icon-btn" data-ind-criterio-excluir="${criterio.id}" title="Excluir">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</td>
</tr>`
)
.join("");
}
function renderPercentuais() {
const body = document.getElementById("ind-percentuais-body");
const filtrados =
percentualFiltroDepartamento === PID_IND_FILTRO_TODOS
? percentuais
: percentuais.filter((p) => p.departamento === percentualFiltroDepartamento);
body.innerHTML = filtrados
.map(
(percentual) => `
<tr>
<td>${pidIndEscapeHtml(percentual.departamento_nome)}</td>
<td>${percentual.tipo_label}</td>
<td>${pidIndFormatPercentual(percentual.percentual_individual)}</td>
<td>${pidIndFormatPercentual(percentual.percentual_grupo)}</td>
<td>${pidIndFormatPercentual(percentual.percentual_departamento)}</td>
<td>${pidIndFormatData(percentual.vigente_desde)}</td>
<td>${pidIndEscapeHtml(percentual.criado_por_nome || "-")}</td>
<td class="pa-row-actions">
<button type="button" class="icon-btn" data-ind-percentual-excluir="${percentual.id}" title="Excluir">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</td>
</tr>`
)
.join("");
}
// ---- Departamentos (cadastro + relação gerente→departamento) ----
// Substituiu o antigo "Apelidos de Setor": em vez de forçar o setor de um
// colaborador específico por nome, o departamento de cada colaborador vem
// do gerente dele (IndicadorDepartamentoGerente) — mantido manualmente por
// ora (ver CLAUDE.md), sem inferir mais nada da planilha.
function gerentesDoDepartamento(departamentoId) {
return departamentoGerentesConfig.filter((g) => g.departamento === departamentoId);
}
function renderDepartamentosConfig() {
const body = document.getElementById("ind-departamentos-body");
body.innerHTML = departamentosConfig
.map((departamento) => {
const gerentes = gerentesDoDepartamento(departamento.id);
const gerentesTexto = gerentes.length
? gerentes.map((g) => pidIndEscapeHtml(g.nome_gerente)).join(", ")
: "—";
return `
<tr>
<td>${pidIndEscapeHtml(departamento.nome)}</td>
<td>${gerentesTexto}</td>
<td><span class="status-pill ${departamento.ativo ? "status-pill--ativo" : "status-pill--inativo"}">${departamento.ativo ? "Ativo" : "Inativo"}</span></td>
<td class="pa-row-actions">
<button type="button" class="btn-outline" data-ind-departamento-gerentes="${departamento.id}">Gerenciar Gerentes</button>
<button type="button" class="icon-btn" data-ind-departamento-editar="${departamento.id}" title="Editar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9" stroke-linecap="round"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button type="button" class="icon-btn" data-ind-departamento-excluir="${departamento.id}" title="Excluir">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</td>
</tr>`;
})
.join("");
}
document.getElementById("ind-criterios-body").addEventListener("click", async (event) => {
const toggleBtn = event.target.closest("[data-ind-criterio-toggle]");
if (toggleBtn) {
const id = Number(toggleBtn.getAttribute("data-ind-criterio-toggle"));
const criterio = criterios.find((c) => c.id === id);
await pidAtualizarCriterioIndicador(id, { ativo: !criterio.ativo });
await carregarConfig();
return;
}
const excluirBtn = event.target.closest("[data-ind-criterio-excluir]");
if (excluirBtn) {
if (!(await pidConfirm("Excluir este critério?", { perigoso: true }))) return;
await pidExcluirCriterioIndicador(Number(excluirBtn.getAttribute("data-ind-criterio-excluir")));
await carregarConfig();
}
});
document.getElementById("ind-criterio-departamento-filtro").addEventListener("change", (event) => {
criterioFiltroDepartamento = event.target.value === PID_IND_FILTRO_TODOS ? PID_IND_FILTRO_TODOS : Number(event.target.value);
renderCriterios();
});
document.getElementById("ind-percentuais-body").addEventListener("click", async (event) => {
const excluirBtn = event.target.closest("[data-ind-percentual-excluir]");
if (excluirBtn) {
if (!(await pidConfirm("Excluir este registro do histórico?", { perigoso: true }))) return;
await pidExcluirPercentualIndicador(Number(excluirBtn.getAttribute("data-ind-percentual-excluir")));
await carregarConfig();
}
});
document.getElementById("ind-percentual-departamento-filtro").addEventListener("change", (event) => {
percentualFiltroDepartamento = event.target.value === PID_IND_FILTRO_TODOS ? PID_IND_FILTRO_TODOS : Number(event.target.value);
renderPercentuais();
});
// -- Modal: Critério --
const criterioModal = document.getElementById("ind-criterio-modal");
document.getElementById("ind-criterio-add-btn").addEventListener("click", () => {
document.getElementById("ind-criterio-modal-title").textContent = "Adicionar Critério";
document.getElementById("ind-criterio-departamento").innerHTML = departamentosConfig
.map((d) => `<option value="${d.id}">${pidIndEscapeHtml(d.nome)}</option>`)
.join("");
if (criterioFiltroDepartamento !== PID_IND_FILTRO_TODOS) {
document.getElementById("ind-criterio-departamento").value = criterioFiltroDepartamento;
}
document.getElementById("ind-criterio-nome").value = "";
document.getElementById("ind-criterio-grupo").value = "individual";
document.getElementById("ind-criterio-peso").value = "";
document.getElementById("ind-criterio-periodo").value = "todos";
document.getElementById("ind-criterio-papel").value = "";
document.getElementById("ind-criterio-calculo").value = "";
document.getElementById("ind-criterio-limiar").value = "90";
document.getElementById("ind-criterio-ativo").checked = true;
document.getElementById("ind-criterio-error").textContent = "";
criterioModal.hidden = false;
});
document.getElementById("ind-criterio-cancel-btn").addEventListener("click", () => {
criterioModal.hidden = true;
});
criterioModal.addEventListener("click", (event) => {
if (event.target === criterioModal) criterioModal.hidden = true;
});
document.getElementById("ind-criterio-save-btn").addEventListener("click", async () => {
const errorEl = document.getElementById("ind-criterio-error");
errorEl.textContent = "";
const peso = document.getElementById("ind-criterio-peso").value.replace(",", ".");
if (!document.getElementById("ind-criterio-nome").value.trim() || !peso) {
errorEl.textContent = "Informe o nome e o peso do critério.";
return;
}
try {
await pidCriarCriterioIndicador({
departamento: Number(document.getElementById("ind-criterio-departamento").value),
nome: document.getElementById("ind-criterio-nome").value.trim(),
grupo: document.getElementById("ind-criterio-grupo").value,
peso,
periodo: document.getElementById("ind-criterio-periodo").value,
papel_aplicavel: document.getElementById("ind-criterio-papel").value,
calculo_automatico: document.getElementById("ind-criterio-calculo").value,
limiar_percentual: document.getElementById("ind-criterio-limiar").value.replace(",", ".") || "90",
ativo: document.getElementById("ind-criterio-ativo").checked,
});
criterioModal.hidden = true;
await carregarConfig();
} catch (e) {
errorEl.textContent = e.message;
}
});
// -- Modal: Percentual por tipo --
const percentualModal = document.getElementById("ind-percentual-modal");
document.getElementById("ind-percentual-add-btn").addEventListener("click", () => {
document.getElementById("ind-percentual-departamento").innerHTML = departamentosConfig
.map((d) => `<option value="${d.id}">${pidIndEscapeHtml(d.nome)}</option>`)
.join("");
if (percentualFiltroDepartamento !== PID_IND_FILTRO_TODOS) {
document.getElementById("ind-percentual-departamento").value = percentualFiltroDepartamento;
}
document.getElementById("ind-percentual-tipo").value = "1";
document.getElementById("ind-percentual-individual").value = "";
document.getElementById("ind-percentual-grupo").value = "20";
document.getElementById("ind-percentual-departamento-valor").value = "20";
document.getElementById("ind-percentual-vigente").value = new Date().toISOString().slice(0, 10);
document.getElementById("ind-percentual-error").textContent = "";
percentualModal.hidden = false;
});
document.getElementById("ind-percentual-cancel-btn").addEventListener("click", () => {
percentualModal.hidden = true;
});
percentualModal.addEventListener("click", (event) => {
if (event.target === percentualModal) percentualModal.hidden = true;
});
document.getElementById("ind-percentual-save-btn").addEventListener("click", async () => {
const errorEl = document.getElementById("ind-percentual-error");
errorEl.textContent = "";
const individual = document.getElementById("ind-percentual-individual").value.replace(",", ".");
const grupo = document.getElementById("ind-percentual-grupo").value.replace(",", ".");
const departamentoValor = document.getElementById("ind-percentual-departamento-valor").value.replace(",", ".");
const vigente = document.getElementById("ind-percentual-vigente").value;
if (!individual || !grupo || !departamentoValor || !vigente) {
errorEl.textContent = "Preencha todos os campos.";
return;
}
try {
await pidCriarPercentualIndicador({
departamento: Number(document.getElementById("ind-percentual-departamento").value),
tipo: document.getElementById("ind-percentual-tipo").value,
percentual_individual: individual,
percentual_grupo: grupo,
percentual_departamento: departamentoValor,
vigente_desde: vigente,
});
percentualModal.hidden = true;
await carregarConfig();
} catch (e) {
errorEl.textContent = e.message;
}
});
// -- Modal: Departamento --
const departamentoModal = document.getElementById("ind-departamento-modal");
let departamentoEditandoId = null;
function abrirDepartamentoModal(departamento) {
departamentoEditandoId = departamento ? departamento.id : null;
document.getElementById("ind-departamento-modal-title").textContent = departamento
? "Editar Departamento"
: "Adicionar Departamento";
document.getElementById("ind-departamento-nome").value = departamento ? departamento.nome : "";
document.getElementById("ind-departamento-ativo").checked = departamento ? departamento.ativo : true;
document.getElementById("ind-departamento-error").textContent = "";
departamentoModal.hidden = false;
}
document.getElementById("ind-departamento-add-btn").addEventListener("click", () => abrirDepartamentoModal(null));
document.getElementById("ind-departamento-cancel-btn").addEventListener("click", () => {
departamentoModal.hidden = true;
});
departamentoModal.addEventListener("click", (event) => {
if (event.target === departamentoModal) departamentoModal.hidden = true;
});
document.getElementById("ind-departamento-save-btn").addEventListener("click", async () => {
const errorEl = document.getElementById("ind-departamento-error");
errorEl.textContent = "";
const nome = document.getElementById("ind-departamento-nome").value.trim();
if (!nome) {
errorEl.textContent = "Informe o nome do departamento.";
return;
}
const ativo = document.getElementById("ind-departamento-ativo").checked;
try {
if (departamentoEditandoId) {
await pidAtualizarDepartamentoIndicador(departamentoEditandoId, { nome, ativo });
} else {
await pidCriarDepartamentoIndicador({ nome, ativo });
}
departamentoModal.hidden = true;
await carregarConfig();
} catch (e) {
errorEl.textContent = e.message;
}
});
// -- Modal: Gerenciar Gerentes (relação gerente→departamento) --
const departamentoGerentesModal = document.getElementById("ind-departamento-gerentes-modal");
let departamentoGerentesAtualId = null;
function renderDepartamentoGerentesLista() {
const box = document.getElementById("ind-departamento-gerentes-lista");
const gerentes = gerentesDoDepartamento(departamentoGerentesAtualId);
box.innerHTML = gerentes.length
? gerentes
.map(
(g) => `
<div class="checklist-item">
<span class="checklist-item__nome">${pidIndEscapeHtml(g.nome_gerente)}</span>
<button type="button" class="icon-btn" data-ind-departamento-gerente-remover="${g.id}" title="Remover">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>`
)
.join("")
: '<p class="checklist-empty">Nenhum gerente cadastrado ainda.</p>';
}
// Um clique já relaciona o nome sugerido a este departamento — o RH só
// precisa reconhecer o nome (visto na última apuração) e clicar, em vez de
// digitar de novo (ver carregarGerentesSugeridos).
function renderDepartamentoGerentesSugestoes() {
const box = document.getElementById("ind-departamento-gerentes-sugestoes");
box.innerHTML = gerentesSugeridos.length
? gerentesSugeridos
.map(
(nome) => `
<button type="button" class="checklist-item ind-departamento-gerente-sugestao" data-ind-departamento-gerente-sugerido="${pidIndEscapeAttr(nome)}">
<span class="checklist-item__nome">${pidIndEscapeHtml(nome)}</span>
<span class="ind-departamento-gerente-sugestao__acao">+ Adicionar</span>
</button>`
)
.join("")
: '<p class="checklist-empty">Nenhum gerente novo encontrado na última apuração.</p>';
}
document.getElementById("ind-departamentos-body").addEventListener("click", async (event) => {
const gerentesBtn = event.target.closest("[data-ind-departamento-gerentes]");
if (gerentesBtn) {
departamentoGerentesAtualId = Number(gerentesBtn.getAttribute("data-ind-departamento-gerentes"));
const departamento = departamentosConfig.find((d) => d.id === departamentoGerentesAtualId);
document.getElementById("ind-departamento-gerentes-modal-title").textContent = `Gerenciar Gerentes — ${departamento.nome}`;
document.getElementById("ind-departamento-gerente-novo").value = "";
document.getElementById("ind-departamento-gerentes-error").textContent = "";
renderDepartamentoGerentesLista();
renderDepartamentoGerentesSugestoes();
departamentoGerentesModal.hidden = false;
return;
}
const editarBtn = event.target.closest("[data-ind-departamento-editar]");
if (editarBtn) {
abrirDepartamentoModal(
departamentosConfig.find((d) => d.id === Number(editarBtn.getAttribute("data-ind-departamento-editar")))
);
return;
}
const excluirBtn = event.target.closest("[data-ind-departamento-excluir]");
if (excluirBtn) {
if (
!(await pidConfirm("Excluir este departamento? Os critérios e percentuais dele também serão excluídos.", {
perigoso: true,
}))
)
return;
await pidExcluirDepartamentoIndicador(Number(excluirBtn.getAttribute("data-ind-departamento-excluir")));
await carregarConfig();
}
});
document.getElementById("ind-departamento-gerentes-close-btn").addEventListener("click", () => {
departamentoGerentesModal.hidden = true;
});
departamentoGerentesModal.addEventListener("click", (event) => {
if (event.target === departamentoGerentesModal) departamentoGerentesModal.hidden = true;
});
document.getElementById("ind-departamento-gerente-add-btn").addEventListener("click", async () => {
const errorEl = document.getElementById("ind-departamento-gerentes-error");
errorEl.textContent = "";
const nomeGerente = document.getElementById("ind-departamento-gerente-novo").value.trim();
if (!nomeGerente) {
errorEl.textContent = "Informe o nome do gerente.";
return;
}
try {
await pidCriarDepartamentoGerenteIndicador({ departamento: departamentoGerentesAtualId, nome_gerente: nomeGerente });
document.getElementById("ind-departamento-gerente-novo").value = "";
departamentoGerentesConfig = await pidFetchDepartamentoGerentesIndicador();
await carregarGerentesSugeridos();
renderDepartamentoGerentesLista();
renderDepartamentoGerentesSugestoes();
renderDepartamentosConfig();
} catch (e) {
errorEl.textContent = e.message;
}
});
document.getElementById("ind-departamento-gerentes-sugestoes").addEventListener("click", async (event) => {
const sugestaoBtn = event.target.closest("[data-ind-departamento-gerente-sugerido]");
if (!sugestaoBtn) return;
const errorEl = document.getElementById("ind-departamento-gerentes-error");
errorEl.textContent = "";
try {
await pidCriarDepartamentoGerenteIndicador({
departamento: departamentoGerentesAtualId,
nome_gerente: sugestaoBtn.getAttribute("data-ind-departamento-gerente-sugerido"),
});
departamentoGerentesConfig = await pidFetchDepartamentoGerentesIndicador();
await carregarGerentesSugeridos();
renderDepartamentoGerentesLista();
renderDepartamentoGerentesSugestoes();
renderDepartamentosConfig();
} catch (e) {
errorEl.textContent = e.message;
}
});
document.getElementById("ind-departamento-gerentes-lista").addEventListener("click", async (event) => {
const removerBtn = event.target.closest("[data-ind-departamento-gerente-remover]");
if (!removerBtn) return;
try {
await pidExcluirDepartamentoGerenteIndicador(Number(removerBtn.getAttribute("data-ind-departamento-gerente-remover")));
departamentoGerentesConfig = await pidFetchDepartamentoGerentesIndicador();
await carregarGerentesSugeridos();
renderDepartamentoGerentesLista();
renderDepartamentoGerentesSugestoes();
renderDepartamentosConfig();
} catch (e) {
document.getElementById("ind-departamento-gerentes-error").textContent = e.message;
}
});
await carregarLista();
});