2200 lines
99 KiB
JavaScript
2200 lines
99 KiB
JavaScript
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");
|
||
}
|
||
|
||
// Delegado ao `pidEscapeHtml` (api.js), que também escapa aspas (a versão
|
||
// antiga por `textContent` não escapava, e a função é usada em atributos).
|
||
function pidIndEscapeHtml(texto) {
|
||
return pidEscapeHtml(texto);
|
||
}
|
||
|
||
function pidIndEscapeAttr(texto) {
|
||
return pidEscapeHtml(texto);
|
||
}
|
||
|
||
// "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"}">${pidIndEscapeHtml(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(" · ");
|
||
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>${pidIndEscapeHtml(PID_IND_GRUPO_LABELS[criterio.grupo] || criterio.grupo)}</td>
|
||
<td>${pidIndFormatPercentual(criterio.peso)}</td>
|
||
<td>${pidIndEscapeHtml(PID_IND_PERIODO_LABELS[criterio.periodo] || criterio.periodo)}</td>
|
||
<td>${criterio.papel_aplicavel ? PID_IND_TIPO_LABELS[criterio.papel_aplicavel] : "Qualquer"}</td>
|
||
<td>${pidIndEscapeHtml(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>${pidIndEscapeHtml(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();
|
||
});
|