feat: implement comparative balance calculation for entry/exit pairs in registro-pontos page
- Added a new function `calcularSaldoComparativoPorPar` to compute comparative balances for entry and exit pairs, enhancing the accuracy of time management. - Updated the logic to handle expected and actual records, allowing for better visibility of discrepancies in worked hours. - Enhanced the table rendering to display comparative balances, improving user experience and clarity in time tracking.
This commit is contained in:
@@ -596,6 +596,149 @@
|
|||||||
return saldos;
|
return saldos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcula saldos comparativos por par entrada/saída
|
||||||
|
* Compara horários reais com horários esperados configurados
|
||||||
|
* Retorna mapa com saldo trabalhado, esperado e diferença
|
||||||
|
*/
|
||||||
|
function calcularSaldoComparativoPorPar(
|
||||||
|
registros: Array<{ tipo: string; hora: number; minuto: number }>,
|
||||||
|
config: {
|
||||||
|
horarioEntrada: string;
|
||||||
|
horarioSaidaAlmoco: string;
|
||||||
|
horarioRetornoAlmoco: string;
|
||||||
|
horarioSaida: string;
|
||||||
|
}
|
||||||
|
): Map<number, {
|
||||||
|
trabalhadoMinutos: number;
|
||||||
|
trabalhadoHoras: number;
|
||||||
|
trabalhadoMinutosResto: number;
|
||||||
|
esperadoMinutos: number;
|
||||||
|
esperadoHoras: number;
|
||||||
|
esperadoMinutosResto: number;
|
||||||
|
diferencaMinutos: number;
|
||||||
|
diferencaHoras: number;
|
||||||
|
diferencaMinutosResto: number;
|
||||||
|
parIndex: number;
|
||||||
|
tamanhoPar: number;
|
||||||
|
}> {
|
||||||
|
const saldos = new Map<number, {
|
||||||
|
trabalhadoMinutos: number;
|
||||||
|
trabalhadoHoras: number;
|
||||||
|
trabalhadoMinutosResto: number;
|
||||||
|
esperadoMinutos: number;
|
||||||
|
esperadoHoras: number;
|
||||||
|
esperadoMinutosResto: number;
|
||||||
|
diferencaMinutos: number;
|
||||||
|
diferencaHoras: number;
|
||||||
|
diferencaMinutosResto: number;
|
||||||
|
parIndex: number;
|
||||||
|
tamanhoPar: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
if (registros.length === 0) return saldos;
|
||||||
|
|
||||||
|
// Parsear horários esperados da configuração
|
||||||
|
const [horaEntradaEsperada, minutoEntradaEsperado] = config.horarioEntrada.split(':').map(Number);
|
||||||
|
const [horaSaidaAlmocoEsperada, minutoSaidaAlmocoEsperado] = config.horarioSaidaAlmoco.split(':').map(Number);
|
||||||
|
const [horaRetornoAlmocoEsperado, minutoRetornoAlmocoEsperado] = config.horarioRetornoAlmoco.split(':').map(Number);
|
||||||
|
const [horaSaidaEsperada, minutoSaidaEsperado] = config.horarioSaida.split(':').map(Number);
|
||||||
|
|
||||||
|
// Ordenar registros por hora e minuto
|
||||||
|
const registrosOrdenados = [...registros].sort((a, b) => {
|
||||||
|
if (a.hora !== b.hora) {
|
||||||
|
return a.hora - b.hora;
|
||||||
|
}
|
||||||
|
return a.minuto - b.minuto;
|
||||||
|
});
|
||||||
|
|
||||||
|
let parIndex = 0;
|
||||||
|
let entradaAtual: { tipo: string; hora: number; minuto: number; index: number } | null = null;
|
||||||
|
let indicesPar: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < registrosOrdenados.length; i++) {
|
||||||
|
const reg = registrosOrdenados[i];
|
||||||
|
|
||||||
|
// Identificar início de um par (entrada ou retorno_almoco)
|
||||||
|
if (reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco') {
|
||||||
|
// Se havia um par anterior incompleto, limpar
|
||||||
|
if (entradaAtual && indicesPar.length > 0) {
|
||||||
|
indicesPar = [];
|
||||||
|
}
|
||||||
|
entradaAtual = { ...reg, index: i };
|
||||||
|
indicesPar = [i];
|
||||||
|
}
|
||||||
|
// Identificar fim de um par (saida_almoco ou saida)
|
||||||
|
else if ((reg.tipo === 'saida_almoco' || reg.tipo === 'saida') && entradaAtual) {
|
||||||
|
indicesPar.push(i);
|
||||||
|
|
||||||
|
// Calcular tempo trabalhado real (saída - entrada)
|
||||||
|
const minutosEntradaReal = entradaAtual.hora * 60 + entradaAtual.minuto;
|
||||||
|
const minutosSaidaReal = reg.hora * 60 + reg.minuto;
|
||||||
|
let trabalhadoMinutos = minutosSaidaReal - minutosEntradaReal;
|
||||||
|
if (trabalhadoMinutos < 0) {
|
||||||
|
trabalhadoMinutos += 24 * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular tempo esperado baseado no tipo de par
|
||||||
|
let esperadoMinutos: number;
|
||||||
|
if (entradaAtual.tipo === 'entrada') {
|
||||||
|
// Par 1: entrada -> saida_almoco
|
||||||
|
const minutosEntradaEsperada = horaEntradaEsperada * 60 + minutoEntradaEsperado;
|
||||||
|
const minutosSaidaEsperada = horaSaidaAlmocoEsperada * 60 + minutoSaidaAlmocoEsperado;
|
||||||
|
esperadoMinutos = minutosSaidaEsperada - minutosEntradaEsperada;
|
||||||
|
if (esperadoMinutos < 0) {
|
||||||
|
esperadoMinutos += 24 * 60;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Par 2: retorno_almoco -> saida
|
||||||
|
const minutosEntradaEsperada = horaRetornoAlmocoEsperado * 60 + minutoRetornoAlmocoEsperado;
|
||||||
|
const minutosSaidaEsperada = horaSaidaEsperada * 60 + minutoSaidaEsperado;
|
||||||
|
esperadoMinutos = minutosSaidaEsperada - minutosEntradaEsperada;
|
||||||
|
if (esperadoMinutos < 0) {
|
||||||
|
esperadoMinutos += 24 * 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular diferença (trabalhado - esperado)
|
||||||
|
const diferencaMinutos = trabalhadoMinutos - esperadoMinutos;
|
||||||
|
|
||||||
|
// Converter para horas e minutos
|
||||||
|
const trabalhadoHoras = Math.floor(trabalhadoMinutos / 60);
|
||||||
|
const trabalhadoMinutosResto = trabalhadoMinutos % 60;
|
||||||
|
|
||||||
|
const esperadoHoras = Math.floor(esperadoMinutos / 60);
|
||||||
|
const esperadoMinutosResto = esperadoMinutos % 60;
|
||||||
|
|
||||||
|
const diferencaHoras = Math.floor(Math.abs(diferencaMinutos) / 60);
|
||||||
|
const diferencaMinutosResto = Math.abs(diferencaMinutos) % 60;
|
||||||
|
|
||||||
|
// Associar saldo a todos os registros do par
|
||||||
|
for (const idx of indicesPar) {
|
||||||
|
saldos.set(idx, {
|
||||||
|
trabalhadoMinutos,
|
||||||
|
trabalhadoHoras,
|
||||||
|
trabalhadoMinutosResto,
|
||||||
|
esperadoMinutos,
|
||||||
|
esperadoHoras,
|
||||||
|
esperadoMinutosResto,
|
||||||
|
diferencaMinutos,
|
||||||
|
diferencaHoras,
|
||||||
|
diferencaMinutosResto,
|
||||||
|
parIndex,
|
||||||
|
tamanhoPar: indicesPar.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parIndex++;
|
||||||
|
entradaAtual = null;
|
||||||
|
indicesPar = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return saldos;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gera array de todas as datas do período selecionado
|
* Gera array de todas as datas do período selecionado
|
||||||
*/
|
*/
|
||||||
@@ -868,9 +1011,17 @@
|
|||||||
|
|
||||||
if (sections.alteracoesGestor) {
|
if (sections.alteracoesGestor) {
|
||||||
try {
|
try {
|
||||||
homologacoes = await client.query(api.pontos.listarHomologacoes, {
|
const todasHomologacoes = await client.query(api.pontos.listarHomologacoes, {
|
||||||
funcionarioId,
|
funcionarioId,
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
|
// Filtrar homologações pelo período selecionado
|
||||||
|
const dataInicioTimestamp = new Date(dataInicio + 'T00:00:00').getTime();
|
||||||
|
const dataFimTimestamp = new Date(dataFim + 'T23:59:59').getTime();
|
||||||
|
|
||||||
|
homologacoes = todasHomologacoes.filter((h) => {
|
||||||
|
return h.criadoEm >= dataInicioTimestamp && h.criadoEm <= dataFimTimestamp;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Erro ao buscar homologações:', error);
|
console.warn('Erro ao buscar homologações:', error);
|
||||||
// Continuar mesmo se houver erro ao buscar homologações
|
// Continuar mesmo se houver erro ao buscar homologações
|
||||||
@@ -879,10 +1030,22 @@
|
|||||||
|
|
||||||
if (sections.dispensasRegistro) {
|
if (sections.dispensasRegistro) {
|
||||||
try {
|
try {
|
||||||
dispensas = await client.query(api.pontos.listarDispensas, {
|
const todasDispensas = await client.query(api.pontos.listarDispensas, {
|
||||||
funcionarioId,
|
funcionarioId,
|
||||||
apenasAtivas: false,
|
apenasAtivas: false,
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
|
// Filtrar dispensas que têm interseção com o período selecionado
|
||||||
|
const dataInicioPeriodo = new Date(dataInicio + 'T00:00:00');
|
||||||
|
const dataFimPeriodo = new Date(dataFim + 'T23:59:59');
|
||||||
|
|
||||||
|
dispensas = todasDispensas.filter((d) => {
|
||||||
|
const dispensaInicio = new Date(d.dataInicio + 'T00:00:00');
|
||||||
|
const dispensaFim = new Date(d.dataFim + 'T23:59:59');
|
||||||
|
|
||||||
|
// Verificar se há interseção entre os períodos
|
||||||
|
return dispensaInicio <= dataFimPeriodo && dispensaFim >= dataInicioPeriodo;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Erro ao buscar dispensas:', error);
|
console.warn('Erro ao buscar dispensas:', error);
|
||||||
// Continuar mesmo se houver erro ao buscar dispensas
|
// Continuar mesmo se houver erro ao buscar dispensas
|
||||||
@@ -890,7 +1053,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Variável para armazenar saldos diários (usada no resumo do banco de horas)
|
// Variável para armazenar saldos diários (usada no resumo do banco de horas)
|
||||||
const saldosDiariosPorData: Record<string, number> = {};
|
const saldosDiariosPorData: Record<string, {
|
||||||
|
diferencaMinutos: number;
|
||||||
|
trabalhadoMinutos: number;
|
||||||
|
esperadoMinutos: number;
|
||||||
|
}> = {};
|
||||||
|
|
||||||
// Tabela de registros
|
// Tabela de registros
|
||||||
if (sections.registrosPonto) {
|
if (sections.registrosPonto) {
|
||||||
@@ -990,42 +1157,134 @@
|
|||||||
return a.minuto - b.minuto;
|
return a.minuto - b.minuto;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calcular saldos por par entrada/saída (apenas com registros reais)
|
// Calcular saldos comparativos por par entrada/saída (apenas com registros reais)
|
||||||
const regsReaisOrdenados = [...regsReais].sort((a, b) => {
|
const regsReaisOrdenados = [...regsReais].sort((a, b) => {
|
||||||
if (a.hora !== b.hora) return a.hora - b.hora;
|
if (a.hora !== b.hora) return a.hora - b.hora;
|
||||||
return a.minuto - b.minuto;
|
return a.minuto - b.minuto;
|
||||||
});
|
});
|
||||||
const saldosPorPar = calcularSaldosPorPar(regsReaisOrdenados);
|
const saldosComparativosPorPar = calcularSaldoComparativoPorPar(regsReaisOrdenados, config);
|
||||||
|
|
||||||
// Calcular saldos esperados para pares incompletos ou dias sem registros
|
// Calcular saldos esperados para pares incompletos ou dias sem registros
|
||||||
const saldosEsperadosPorPar: Map<number, { saldoMinutos: number; horas: number; minutos: number; tamanhoPar: number; incompleto: boolean }> = new Map();
|
const saldosEsperadosPorPar: Map<number, {
|
||||||
|
trabalhadoMinutos: number;
|
||||||
|
trabalhadoHoras: number;
|
||||||
|
trabalhadoMinutosResto: number;
|
||||||
|
esperadoMinutos: number;
|
||||||
|
esperadoHoras: number;
|
||||||
|
esperadoMinutosResto: number;
|
||||||
|
diferencaMinutos: number;
|
||||||
|
diferencaHoras: number;
|
||||||
|
diferencaMinutosResto: number;
|
||||||
|
tamanhoPar: number;
|
||||||
|
incompleto: boolean;
|
||||||
|
}> = new Map();
|
||||||
|
|
||||||
|
// Calcular saldo diário total (diferença acumulada de todos os pares)
|
||||||
|
// Declarar variáveis ANTES de usá-las
|
||||||
|
let saldoDiarioTotalDiferencaMinutos = 0;
|
||||||
|
let saldoDiarioTotalTrabalhadoMinutos = 0;
|
||||||
|
let saldoDiarioTotalEsperadoMinutos = 0;
|
||||||
|
|
||||||
|
// Criar conjunto de chaves de registros que já foram processados como completos
|
||||||
|
// Isso será usado tanto para evitar criar pares incompletos quanto para evitar somar duplicados
|
||||||
|
const chavesProcessadasCompletas = new Set<string>();
|
||||||
|
const paresCompletosProcessados = new Set<number>(); // Rastrear parIndex já processados
|
||||||
|
|
||||||
|
// Criar conjunto de chaves e somar saldos dos pares completos
|
||||||
|
// IMPORTANTE: saldosComparativosPorPar associa o saldo a AMBOS os registros do par (entrada + saída)
|
||||||
|
// Então precisamos garantir que somamos apenas UMA VEZ por par, não uma vez por registro
|
||||||
|
for (const [index, saldo] of saldosComparativosPorPar.entries()) {
|
||||||
|
const regEntrada = regsReaisOrdenados[index];
|
||||||
|
if (regEntrada) {
|
||||||
|
// Verificar se este parIndex já foi processado
|
||||||
|
if (!paresCompletosProcessados.has(saldo.parIndex)) {
|
||||||
|
// Primeira vez processando este par, somar o saldo
|
||||||
|
saldoDiarioTotalDiferencaMinutos += saldo.diferencaMinutos;
|
||||||
|
saldoDiarioTotalTrabalhadoMinutos += saldo.trabalhadoMinutos;
|
||||||
|
saldoDiarioTotalEsperadoMinutos += saldo.esperadoMinutos;
|
||||||
|
paresCompletosProcessados.add(saldo.parIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adicionar chaves para evitar processamento duplicado depois
|
||||||
|
const chaveEntrada = `${regEntrada.tipo}-${regEntrada.hora}-${regEntrada.minuto}`;
|
||||||
|
chavesProcessadasCompletas.add(chaveEntrada);
|
||||||
|
// Encontrar saída correspondente
|
||||||
|
const tipoSaidaEsperado = regEntrada.tipo === 'entrada' ? 'saida_almoco' : 'saida';
|
||||||
|
for (let j = index + 1; j < regsReaisOrdenados.length; j++) {
|
||||||
|
const regSaida = regsReaisOrdenados[j];
|
||||||
|
if (regSaida && regSaida.tipo === tipoSaidaEsperado) {
|
||||||
|
const chaveSaida = `${regSaida.tipo}-${regSaida.hora}-${regSaida.minuto}`;
|
||||||
|
chavesProcessadasCompletas.add(chaveSaida);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Identificar pares incompletos e calcular saldos esperados
|
// Identificar pares incompletos e calcular saldos esperados
|
||||||
|
// IMPORTANTE: Só processar pares que NÃO foram processados como completos
|
||||||
for (let i = 0; i < todosRegistros.length; i++) {
|
for (let i = 0; i < todosRegistros.length; i++) {
|
||||||
const reg = todosRegistros[i];
|
const reg = todosRegistros[i];
|
||||||
|
|
||||||
// Se é entrada ou retorno_almoco (início de par) e é real
|
// Se é entrada ou retorno_almoco (início de par) e é real
|
||||||
if ((reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco') && reg.real) {
|
if ((reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco') && reg.real) {
|
||||||
|
// Verificar se este registro já foi processado como par completo
|
||||||
|
const chaveRegistro = `${reg.tipo}-${reg.hora}-${reg.minuto}`;
|
||||||
|
if (chavesProcessadasCompletas.has(chaveRegistro)) {
|
||||||
|
continue; // Já foi processado como par completo, pular
|
||||||
|
}
|
||||||
|
|
||||||
// Verificar se há saída correspondente real
|
// Verificar se há saída correspondente real
|
||||||
const tipoSaidaEsperado = reg.tipo === 'entrada' ? 'saida_almoco' : 'saida';
|
const tipoSaidaEsperado = reg.tipo === 'entrada' ? 'saida_almoco' : 'saida';
|
||||||
const saidaEncontrada = regsReais.find(r => r.tipo === tipoSaidaEsperado);
|
const saidaEncontrada = regsReais.find(r => r.tipo === tipoSaidaEsperado);
|
||||||
|
|
||||||
|
// Se há saída correspondente, verificar se ela também está em chavesProcessadasCompletas
|
||||||
|
// Se estiver, significa que este par já foi processado como completo
|
||||||
|
if (saidaEncontrada) {
|
||||||
|
const chaveSaida = `${saidaEncontrada.tipo}-${saidaEncontrada.hora}-${saidaEncontrada.minuto}`;
|
||||||
|
if (chavesProcessadasCompletas.has(chaveSaida)) {
|
||||||
|
continue; // Par completo já processado, pular
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!saidaEncontrada) {
|
if (!saidaEncontrada) {
|
||||||
// Par incompleto: calcular saldo esperado baseado na configuração
|
// Par incompleto: entrada real sem saída correspondente
|
||||||
|
// NÃO calcular tempo trabalhado aqui porque não há saída marcada
|
||||||
|
// O tempo trabalhado será 0, e a diferença será negativa (0 - esperado)
|
||||||
const regEsperado = regsEsperados.find(r => r.tipo === tipoSaidaEsperado);
|
const regEsperado = regsEsperados.find(r => r.tipo === tipoSaidaEsperado);
|
||||||
if (regEsperado) {
|
if (regEsperado) {
|
||||||
const minutosEntrada = reg.hora * 60 + reg.minuto;
|
// Tempo trabalhado = 0 (não há saída marcada, então não podemos assumir tempo trabalhado)
|
||||||
const minutosSaidaEsperada = regEsperado.hora * 60 + regEsperado.minuto;
|
const trabalhadoMinutos = 0;
|
||||||
let saldoMinutos = minutosSaidaEsperada - minutosEntrada;
|
|
||||||
if (saldoMinutos < 0) {
|
// Calcular tempo esperado
|
||||||
saldoMinutos += 24 * 60;
|
let esperadoMinutos: number;
|
||||||
|
if (reg.tipo === 'entrada') {
|
||||||
|
const [horaEntradaEsperada, minutoEntradaEsperado] = config.horarioEntrada.split(':').map(Number);
|
||||||
|
const [horaSaidaAlmocoEsperada, minutoSaidaAlmocoEsperado] = config.horarioSaidaAlmoco.split(':').map(Number);
|
||||||
|
const minutosEntradaEsperada = horaEntradaEsperada * 60 + minutoEntradaEsperado;
|
||||||
|
const minutosSaidaEsperadaConfig = horaSaidaAlmocoEsperada * 60 + minutoSaidaAlmocoEsperado;
|
||||||
|
esperadoMinutos = minutosSaidaEsperadaConfig - minutosEntradaEsperada;
|
||||||
|
if (esperadoMinutos < 0) esperadoMinutos += 24 * 60;
|
||||||
|
} else {
|
||||||
|
const [horaRetornoAlmocoEsperado, minutoRetornoAlmocoEsperado] = config.horarioRetornoAlmoco.split(':').map(Number);
|
||||||
|
const [horaSaidaEsperada, minutoSaidaEsperado] = config.horarioSaida.split(':').map(Number);
|
||||||
|
const minutosEntradaEsperada = horaRetornoAlmocoEsperado * 60 + minutoRetornoAlmocoEsperado;
|
||||||
|
const minutosSaidaEsperadaConfig = horaSaidaEsperada * 60 + minutoSaidaEsperado;
|
||||||
|
esperadoMinutos = minutosSaidaEsperadaConfig - minutosEntradaEsperada;
|
||||||
|
if (esperadoMinutos < 0) esperadoMinutos += 24 * 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
const horas = Math.floor(saldoMinutos / 60);
|
// Calcular diferença (0 - esperado = negativo)
|
||||||
const minutos = saldoMinutos % 60;
|
const diferencaMinutos = -esperadoMinutos;
|
||||||
|
|
||||||
|
const trabalhadoHoras = 0;
|
||||||
|
const trabalhadoMinutosResto = 0;
|
||||||
|
const esperadoHoras = Math.floor(esperadoMinutos / 60);
|
||||||
|
const esperadoMinutosResto = esperadoMinutos % 60;
|
||||||
|
const diferencaHoras = Math.floor(Math.abs(diferencaMinutos) / 60);
|
||||||
|
const diferencaMinutosResto = Math.abs(diferencaMinutos) % 60;
|
||||||
|
|
||||||
// Contar quantos registros fazem parte deste par na lista todosRegistros
|
// Contar quantos registros fazem parte deste par na lista todosRegistros
|
||||||
// Encontrar índice do registro na lista ordenada todosRegistros
|
|
||||||
const indexNaListaTodos = todosRegistros.findIndex(
|
const indexNaListaTodos = todosRegistros.findIndex(
|
||||||
(r, idx) => idx >= i && r.tipo === reg.tipo && r.hora === reg.hora && r.minuto === reg.minuto
|
(r, idx) => idx >= i && r.tipo === reg.tipo && r.hora === reg.hora && r.minuto === reg.minuto
|
||||||
);
|
);
|
||||||
@@ -1041,9 +1300,15 @@
|
|||||||
|
|
||||||
if (indexNaListaTodos >= 0) {
|
if (indexNaListaTodos >= 0) {
|
||||||
saldosEsperadosPorPar.set(indexNaListaTodos, {
|
saldosEsperadosPorPar.set(indexNaListaTodos, {
|
||||||
saldoMinutos,
|
trabalhadoMinutos,
|
||||||
horas,
|
trabalhadoHoras,
|
||||||
minutos,
|
trabalhadoMinutosResto,
|
||||||
|
esperadoMinutos,
|
||||||
|
esperadoHoras,
|
||||||
|
esperadoMinutosResto,
|
||||||
|
diferencaMinutos,
|
||||||
|
diferencaHoras,
|
||||||
|
diferencaMinutosResto,
|
||||||
tamanhoPar,
|
tamanhoPar,
|
||||||
incompleto: true
|
incompleto: true
|
||||||
});
|
});
|
||||||
@@ -1053,10 +1318,117 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calcular saldo diário total (soma de todos os pares reais)
|
// Identificar pares completamente não marcados (quando há registros reais mas um par completo não foi marcado)
|
||||||
let saldoDiarioTotalMinutos = 0;
|
if (regsReais.length > 0) {
|
||||||
for (const saldo of saldosPorPar.values()) {
|
for (let i = 0; i < todosRegistros.length; i++) {
|
||||||
saldoDiarioTotalMinutos += saldo.saldoMinutos;
|
const reg = todosRegistros[i];
|
||||||
|
|
||||||
|
// Se é entrada ou retorno_almoco (início de par) e NÃO é real
|
||||||
|
if ((reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco') && !reg.real) {
|
||||||
|
// Verificar se a saída correspondente também não foi marcada
|
||||||
|
const tipoSaidaEsperado = reg.tipo === 'entrada' ? 'saida_almoco' : 'saida';
|
||||||
|
const saidaEsperada = todosRegistros.find(
|
||||||
|
(r, idx) => idx > i && r.tipo === tipoSaidaEsperado && !r.real
|
||||||
|
);
|
||||||
|
|
||||||
|
if (saidaEsperada) {
|
||||||
|
// Par completamente não marcado: calcular saldo negativo
|
||||||
|
// Tempo trabalhado = 0, tempo esperado = configurado, diferença = -esperado
|
||||||
|
let esperadoMinutos: number;
|
||||||
|
if (reg.tipo === 'entrada') {
|
||||||
|
const [horaEntradaEsperada, minutoEntradaEsperado] = config.horarioEntrada.split(':').map(Number);
|
||||||
|
const [horaSaidaAlmocoEsperada, minutoSaidaAlmocoEsperado] = config.horarioSaidaAlmoco.split(':').map(Number);
|
||||||
|
const minutosEntradaEsperada = horaEntradaEsperada * 60 + minutoEntradaEsperado;
|
||||||
|
const minutosSaidaEsperadaConfig = horaSaidaAlmocoEsperada * 60 + minutoSaidaAlmocoEsperado;
|
||||||
|
esperadoMinutos = minutosSaidaEsperadaConfig - minutosEntradaEsperada;
|
||||||
|
if (esperadoMinutos < 0) esperadoMinutos += 24 * 60;
|
||||||
|
} else {
|
||||||
|
const [horaRetornoAlmocoEsperado, minutoRetornoAlmocoEsperado] = config.horarioRetornoAlmoco.split(':').map(Number);
|
||||||
|
const [horaSaidaEsperada, minutoSaidaEsperado] = config.horarioSaida.split(':').map(Number);
|
||||||
|
const minutosEntradaEsperada = horaRetornoAlmocoEsperado * 60 + minutoRetornoAlmocoEsperado;
|
||||||
|
const minutosSaidaEsperadaConfig = horaSaidaEsperada * 60 + minutoSaidaEsperado;
|
||||||
|
esperadoMinutos = minutosSaidaEsperadaConfig - minutosEntradaEsperada;
|
||||||
|
if (esperadoMinutos < 0) esperadoMinutos += 24 * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trabalhado = 0, diferença = -esperado
|
||||||
|
const trabalhadoMinutos = 0;
|
||||||
|
const diferencaMinutos = -esperadoMinutos;
|
||||||
|
|
||||||
|
const trabalhadoHoras = 0;
|
||||||
|
const trabalhadoMinutosResto = 0;
|
||||||
|
const esperadoHoras = Math.floor(esperadoMinutos / 60);
|
||||||
|
const esperadoMinutosResto = esperadoMinutos % 60;
|
||||||
|
const diferencaHoras = Math.floor(Math.abs(diferencaMinutos) / 60);
|
||||||
|
const diferencaMinutosResto = Math.abs(diferencaMinutos) % 60;
|
||||||
|
|
||||||
|
// Encontrar índice da saída esperada na lista
|
||||||
|
const indexSaidaEsperada = todosRegistros.findIndex(
|
||||||
|
(r, idx) => idx > i && r.tipo === tipoSaidaEsperado && !r.real
|
||||||
|
);
|
||||||
|
|
||||||
|
// Associar saldo negativo ao início do par (entrada)
|
||||||
|
saldosEsperadosPorPar.set(i, {
|
||||||
|
trabalhadoMinutos,
|
||||||
|
trabalhadoHoras,
|
||||||
|
trabalhadoMinutosResto,
|
||||||
|
esperadoMinutos,
|
||||||
|
esperadoHoras,
|
||||||
|
esperadoMinutosResto,
|
||||||
|
diferencaMinutos,
|
||||||
|
diferencaHoras,
|
||||||
|
diferencaMinutosResto,
|
||||||
|
tamanhoPar: indexSaidaEsperada >= 0 ? 2 : 1, // entrada + saída
|
||||||
|
incompleto: false // Par completo não marcado
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTA: Os saldos dos pares completos já foram somados acima quando criamos chavesProcessadasCompletas
|
||||||
|
// As variáveis saldoDiarioTotal* já foram declaradas e inicializadas acima
|
||||||
|
|
||||||
|
// Somar saldos dos pares completamente não marcados e incompletos
|
||||||
|
// IMPORTANTE: Não somar pares que já foram processados como completos acima
|
||||||
|
// Usar o conjunto chavesProcessadasCompletas criado anteriormente
|
||||||
|
for (const [indexNaListaTodos, saldo] of saldosEsperadosPorPar.entries()) {
|
||||||
|
const regNaListaTodos = todosRegistros[indexNaListaTodos];
|
||||||
|
|
||||||
|
// Verificar se este registro real já foi processado como par completo
|
||||||
|
if (regNaListaTodos && regNaListaTodos.real) {
|
||||||
|
const chaveRegistro = `${regNaListaTodos.tipo}-${regNaListaTodos.hora}-${regNaListaTodos.minuto}`;
|
||||||
|
if (chavesProcessadasCompletas.has(chaveRegistro)) {
|
||||||
|
// Este registro está em chavesProcessadasCompletas
|
||||||
|
// Verificar se há uma saída correspondente que também está lá
|
||||||
|
// Se ambas estão, significa que o par completo já foi processado
|
||||||
|
const tipoSaidaEsperado = regNaListaTodos.tipo === 'entrada' ? 'saida_almoco' : 'saida';
|
||||||
|
|
||||||
|
// Procurar saída correspondente em regsReais que também está em chavesProcessadasCompletas
|
||||||
|
const saidaCompletaEncontrada = regsReais.find(r => {
|
||||||
|
if (r.tipo !== tipoSaidaEsperado) return false;
|
||||||
|
const chaveSaida = `${r.tipo}-${r.hora}-${r.minuto}`;
|
||||||
|
return chavesProcessadasCompletas.has(chaveSaida);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (saidaCompletaEncontrada) {
|
||||||
|
continue; // Par completo já processado (entrada + saída estão em chavesProcessadasCompletas), não somar novamente
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saldo.trabalhadoMinutos === 0 && !saldo.incompleto) {
|
||||||
|
// Par completamente não marcado: adicionar diferença negativa
|
||||||
|
saldoDiarioTotalDiferencaMinutos += saldo.diferencaMinutos;
|
||||||
|
saldoDiarioTotalTrabalhadoMinutos += saldo.trabalhadoMinutos; // 0
|
||||||
|
saldoDiarioTotalEsperadoMinutos += saldo.esperadoMinutos;
|
||||||
|
} else if (saldo.incompleto) {
|
||||||
|
// Par incompleto: entrada real sem saída correspondente
|
||||||
|
// Só somar se não foi processado como completo acima
|
||||||
|
saldoDiarioTotalDiferencaMinutos += saldo.diferencaMinutos;
|
||||||
|
saldoDiarioTotalTrabalhadoMinutos += saldo.trabalhadoMinutos;
|
||||||
|
saldoDiarioTotalEsperadoMinutos += saldo.esperadoMinutos;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se não há registros reais, calcular saldo esperado baseado na configuração
|
// Se não há registros reais, calcular saldo esperado baseado na configuração
|
||||||
@@ -1079,10 +1451,17 @@
|
|||||||
let saldoPar2 = minutosPar2Saida - minutosPar2Entrada;
|
let saldoPar2 = minutosPar2Saida - minutosPar2Entrada;
|
||||||
if (saldoPar2 < 0) saldoPar2 += 24 * 60;
|
if (saldoPar2 < 0) saldoPar2 += 24 * 60;
|
||||||
|
|
||||||
saldoDiarioTotalMinutos = saldoPar1 + saldoPar2;
|
saldoDiarioTotalTrabalhadoMinutos = 0; // Nenhum tempo trabalhado
|
||||||
|
saldoDiarioTotalEsperadoMinutos = saldoPar1 + saldoPar2;
|
||||||
|
saldoDiarioTotalDiferencaMinutos = -saldoDiarioTotalEsperadoMinutos; // Diferença negativa (0 - esperado)
|
||||||
}
|
}
|
||||||
|
|
||||||
saldosDiariosPorData[data] = saldoDiarioTotalMinutos;
|
// Armazenar saldo diário completo (usado no resumo do banco de horas)
|
||||||
|
saldosDiariosPorData[data] = {
|
||||||
|
diferencaMinutos: saldoDiarioTotalDiferencaMinutos,
|
||||||
|
trabalhadoMinutos: saldoDiarioTotalTrabalhadoMinutos,
|
||||||
|
esperadoMinutos: saldoDiarioTotalEsperadoMinutos
|
||||||
|
};
|
||||||
|
|
||||||
// Criar linhas da tabela
|
// Criar linhas da tabela
|
||||||
for (let i = 0; i < todosRegistros.length; i++) {
|
for (let i = 0; i < todosRegistros.length; i++) {
|
||||||
@@ -1105,7 +1484,7 @@
|
|||||||
linha._naoMarcado = true;
|
linha._naoMarcado = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saldo Diário por par entrada/saída
|
// Saldo Diário por par entrada/saída com cálculo comparativo
|
||||||
if (sections.saldoDiario) {
|
if (sections.saldoDiario) {
|
||||||
const isInicioPar = reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco';
|
const isInicioPar = reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco';
|
||||||
|
|
||||||
@@ -1117,17 +1496,23 @@
|
|||||||
// Verificar se há saldo esperado (par incompleto)
|
// Verificar se há saldo esperado (par incompleto)
|
||||||
const saldoEsperado = saldosEsperadosPorPar.get(i);
|
const saldoEsperado = saldosEsperadosPorPar.get(i);
|
||||||
if (saldoEsperado) {
|
if (saldoEsperado) {
|
||||||
// Par incompleto: mostrar saldo esperado em vermelho
|
// Par incompleto: mostrar saldo comparativo em vermelho
|
||||||
linha._saldoVermelho = true;
|
linha._saldoVermelho = true;
|
||||||
|
const sinalDiferenca = saldoEsperado.diferencaMinutos >= 0 ? '+' : '-';
|
||||||
linha.push({
|
linha.push({
|
||||||
content: `+${saldoEsperado.horas}h ${saldoEsperado.minutos}min`,
|
content: `+${saldoEsperado.trabalhadoHoras}h ${saldoEsperado.trabalhadoMinutosResto}min / ${sinalDiferenca}${saldoEsperado.diferencaHoras}h ${saldoEsperado.diferencaMinutosResto}min`,
|
||||||
rowSpan: saldoEsperado.tamanhoPar
|
rowSpan: saldoEsperado.tamanhoPar
|
||||||
});
|
});
|
||||||
} else if (indexReal >= 0) {
|
} else if (indexReal >= 0) {
|
||||||
const saldoPar = saldosPorPar.get(indexReal);
|
const saldoPar = saldosComparativosPorPar.get(indexReal);
|
||||||
if (saldoPar) {
|
if (saldoPar) {
|
||||||
|
const sinalDiferenca = saldoPar.diferencaMinutos >= 0 ? '+' : '-';
|
||||||
|
// Aplicar cor vermelha se diferença for negativa
|
||||||
|
if (saldoPar.diferencaMinutos < 0) {
|
||||||
|
linha._saldoVermelho = true;
|
||||||
|
}
|
||||||
linha.push({
|
linha.push({
|
||||||
content: `+${saldoPar.horas}h ${saldoPar.minutos}min`,
|
content: `+${saldoPar.trabalhadoHoras}h ${saldoPar.trabalhadoMinutosResto}min / ${sinalDiferenca}${saldoPar.diferencaHoras}h ${saldoPar.diferencaMinutosResto}min`,
|
||||||
rowSpan: saldoPar.tamanhoPar
|
rowSpan: saldoPar.tamanhoPar
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -1152,14 +1537,24 @@
|
|||||||
// Verificar se há saldo esperado para este par
|
// Verificar se há saldo esperado para este par
|
||||||
const saldoEsperado = saldosEsperadosPorPar.get(i);
|
const saldoEsperado = saldosEsperadosPorPar.get(i);
|
||||||
if (saldoEsperado) {
|
if (saldoEsperado) {
|
||||||
// Par incompleto: mostrar saldo esperado em vermelho
|
// Par incompleto ou completamente não marcado: mostrar saldo em vermelho
|
||||||
linha._saldoVermelho = true;
|
linha._saldoVermelho = true;
|
||||||
linha.push({
|
const sinalDiferenca = saldoEsperado.diferencaMinutos >= 0 ? '+' : '-';
|
||||||
content: `+${saldoEsperado.horas}h ${saldoEsperado.minutos}min`,
|
|
||||||
rowSpan: saldoEsperado.tamanhoPar
|
// Se par completamente não marcado (trabalhado = 0), mostrar apenas diferença negativa
|
||||||
});
|
if (saldoEsperado.trabalhadoMinutos === 0) {
|
||||||
|
linha.push({
|
||||||
|
content: `+0h 0min / ${sinalDiferenca}${saldoEsperado.diferencaHoras}h ${saldoEsperado.diferencaMinutosResto}min`,
|
||||||
|
rowSpan: saldoEsperado.tamanhoPar
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
linha.push({
|
||||||
|
content: `+${saldoEsperado.trabalhadoHoras}h ${saldoEsperado.trabalhadoMinutosResto}min / ${sinalDiferenca}${saldoEsperado.diferencaHoras}h ${saldoEsperado.diferencaMinutosResto}min`,
|
||||||
|
rowSpan: saldoEsperado.tamanhoPar
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (regsReais.length === 0) {
|
} else if (regsReais.length === 0) {
|
||||||
// Dia sem registros: calcular saldo esperado completo
|
// Dia sem registros: calcular saldo esperado completo com diferença negativa
|
||||||
if (reg.tipo === 'entrada') {
|
if (reg.tipo === 'entrada') {
|
||||||
// Par 1 completo esperado
|
// Par 1 completo esperado
|
||||||
const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number);
|
const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number);
|
||||||
@@ -1171,8 +1566,9 @@
|
|||||||
const horas = Math.floor(saldoMinutos / 60);
|
const horas = Math.floor(saldoMinutos / 60);
|
||||||
const minutos = saldoMinutos % 60;
|
const minutos = saldoMinutos % 60;
|
||||||
linha._saldoVermelho = true;
|
linha._saldoVermelho = true;
|
||||||
|
// Para dia sem registros, mostrar 0h trabalhado e diferença negativa
|
||||||
linha.push({
|
linha.push({
|
||||||
content: `+${horas}h ${minutos}min`,
|
content: `+0h 0min / -${horas}h ${minutos}min`,
|
||||||
rowSpan: 2 // entrada + saida_almoco
|
rowSpan: 2 // entrada + saida_almoco
|
||||||
});
|
});
|
||||||
} else if (reg.tipo === 'retorno_almoco') {
|
} else if (reg.tipo === 'retorno_almoco') {
|
||||||
@@ -1186,19 +1582,53 @@
|
|||||||
const horas = Math.floor(saldoMinutos / 60);
|
const horas = Math.floor(saldoMinutos / 60);
|
||||||
const minutos = saldoMinutos % 60;
|
const minutos = saldoMinutos % 60;
|
||||||
linha._saldoVermelho = true;
|
linha._saldoVermelho = true;
|
||||||
|
// Para dia sem registros, mostrar 0h trabalhado e diferença negativa
|
||||||
linha.push({
|
linha.push({
|
||||||
content: `+${horas}h ${minutos}min`,
|
content: `+0h 0min / -${horas}h ${minutos}min`,
|
||||||
rowSpan: 2 // retorno_almoco + saida
|
rowSpan: 2 // retorno_almoco + saida
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
linha.push('-');
|
linha.push('-');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
linha.push('-');
|
// Há registros reais mas este par não foi marcado completamente
|
||||||
|
// Verificar se é um par completamente não marcado
|
||||||
|
const tipoSaidaEsperado = reg.tipo === 'entrada' ? 'saida_almoco' : 'saida';
|
||||||
|
const saidaEsperadaExiste = todosRegistros.some(
|
||||||
|
(r, idx) => idx > i && r.tipo === tipoSaidaEsperado && !r.real
|
||||||
|
);
|
||||||
|
|
||||||
|
if (saidaEsperadaExiste) {
|
||||||
|
// Par completamente não marcado: calcular saldo negativo
|
||||||
|
const saldoEsperadoCompleto = saldosEsperadosPorPar.get(i);
|
||||||
|
if (saldoEsperadoCompleto) {
|
||||||
|
linha._saldoVermelho = true;
|
||||||
|
const sinalDiferenca = saldoEsperadoCompleto.diferencaMinutos >= 0 ? '+' : '-';
|
||||||
|
linha.push({
|
||||||
|
content: `+0h 0min / ${sinalDiferenca}${saldoEsperadoCompleto.diferencaHoras}h ${saldoEsperadoCompleto.diferencaMinutosResto}min`,
|
||||||
|
rowSpan: saldoEsperadoCompleto.tamanhoPar
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
linha.push('-');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
linha.push('-');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Saída sem entrada correspondente: não tem saldo
|
// Saída não marcada: verificar se faz parte de um par completamente não marcado
|
||||||
linha.push('-');
|
// Se a entrada correspondente também não foi marcada, o saldo já foi adicionado na linha da entrada
|
||||||
|
// Então apenas não adicionar nada aqui (será coberto pelo rowspan)
|
||||||
|
const tipoEntradaEsperado = reg.tipo === 'saida_almoco' ? 'entrada' : 'retorno_almoco';
|
||||||
|
const entradaEsperadaExiste = todosRegistros.some(
|
||||||
|
(r, idx) => idx < i && r.tipo === tipoEntradaEsperado && !r.real
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!entradaEsperadaExiste) {
|
||||||
|
// Saída sem entrada correspondente esperada: não tem saldo
|
||||||
|
linha.push('-');
|
||||||
|
}
|
||||||
|
// Se entrada esperada existe, o saldo já foi adicionado com rowspan na linha da entrada
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1269,12 +1699,48 @@
|
|||||||
funcionarioId,
|
funcionarioId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calcular saldo do período selecionado baseado nos saldos diários calculados
|
// Calcular total de dias do período selecionado
|
||||||
let saldoPeriodoMinutos = 0;
|
const diasPeriodo = gerarDiasPeriodo(dataInicio, dataFim);
|
||||||
|
const totalDiasPeriodo = diasPeriodo.length;
|
||||||
|
|
||||||
|
// Calcular carga horária diária esperada baseada na configuração
|
||||||
|
const [horaEntradaConfig, minutoEntradaConfig] = config.horarioEntrada.split(':').map(Number);
|
||||||
|
const [horaSaidaAlmocoConfig, minutoSaidaAlmocoConfig] = config.horarioSaidaAlmoco.split(':').map(Number);
|
||||||
|
const [horaRetornoAlmocoConfig, minutoRetornoAlmocoConfig] = config.horarioRetornoAlmoco.split(':').map(Number);
|
||||||
|
const [horaSaidaConfig, minutoSaidaConfig] = config.horarioSaida.split(':').map(Number);
|
||||||
|
|
||||||
|
// Par 1: entrada -> saida_almoco
|
||||||
|
const minutosPar1EsperadoConfig = (horaSaidaAlmocoConfig * 60 + minutoSaidaAlmocoConfig) - (horaEntradaConfig * 60 + minutoEntradaConfig);
|
||||||
|
const minutosPar1EsperadoAjustadoConfig = minutosPar1EsperadoConfig < 0 ? minutosPar1EsperadoConfig + 24 * 60 : minutosPar1EsperadoConfig;
|
||||||
|
|
||||||
|
// Par 2: retorno_almoco -> saida
|
||||||
|
const minutosPar2EsperadoConfig = (horaSaidaConfig * 60 + minutoSaidaConfig) - (horaRetornoAlmocoConfig * 60 + minutoRetornoAlmocoConfig);
|
||||||
|
const minutosPar2EsperadoAjustadoConfig = minutosPar2EsperadoConfig < 0 ? minutosPar2EsperadoConfig + 24 * 60 : minutosPar2EsperadoConfig;
|
||||||
|
|
||||||
|
const cargaHorariaDiariaEsperadaMinutos = minutosPar1EsperadoAjustadoConfig + minutosPar2EsperadoAjustadoConfig;
|
||||||
|
|
||||||
|
// Calcular saldos do período selecionado baseado nos saldos diários calculados
|
||||||
|
let saldoPeriodoDiferencaMinutos = 0;
|
||||||
|
let saldoPeriodoTrabalhadoMinutos = 0;
|
||||||
|
let diasComSaldoPositivo = 0;
|
||||||
|
let diasComSaldoNegativo = 0;
|
||||||
|
let diasSemRegistros = 0;
|
||||||
|
|
||||||
if (sections.registrosPonto && Object.keys(saldosDiariosPorData).length > 0) {
|
if (sections.registrosPonto && Object.keys(saldosDiariosPorData).length > 0) {
|
||||||
// Somar todos os saldos diários do período
|
// Somar todos os saldos diários do período
|
||||||
for (const saldoMinutos of Object.values(saldosDiariosPorData)) {
|
for (const saldo of Object.values(saldosDiariosPorData)) {
|
||||||
saldoPeriodoMinutos += saldoMinutos;
|
saldoPeriodoDiferencaMinutos += saldo.diferencaMinutos;
|
||||||
|
saldoPeriodoTrabalhadoMinutos += saldo.trabalhadoMinutos;
|
||||||
|
|
||||||
|
if (saldo.diferencaMinutos > 0) {
|
||||||
|
diasComSaldoPositivo++;
|
||||||
|
} else if (saldo.diferencaMinutos < 0) {
|
||||||
|
diasComSaldoNegativo++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saldo.trabalhadoMinutos === 0 && saldo.esperadoMinutos > 0) {
|
||||||
|
diasSemRegistros++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: calcular a partir dos registros se não tiver saldos diários
|
// Fallback: calcular a partir dos registros se não tiver saldos diários
|
||||||
@@ -1295,19 +1761,50 @@
|
|||||||
for (const regs of Object.values(registrosPorDataPeriodo)) {
|
for (const regs of Object.values(registrosPorDataPeriodo)) {
|
||||||
const saldoDiario = calcularSaldoDiario(regs);
|
const saldoDiario = calcularSaldoDiario(regs);
|
||||||
if (saldoDiario) {
|
if (saldoDiario) {
|
||||||
saldoPeriodoMinutos += saldoDiario.saldoMinutos;
|
saldoPeriodoDiferencaMinutos += saldoDiario.saldoMinutos;
|
||||||
|
saldoPeriodoTrabalhadoMinutos += saldoDiario.saldoMinutos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const horasPeriodo = Math.floor(Math.abs(saldoPeriodoMinutos) / 60);
|
// Calcular saldo esperado do período: carga horária diária × número de dias
|
||||||
const minutosPeriodo = Math.abs(saldoPeriodoMinutos) % 60;
|
const saldoPeriodoEsperadoMinutos = cargaHorariaDiariaEsperadaMinutos * totalDiasPeriodo;
|
||||||
const sinalPeriodo = saldoPeriodoMinutos >= 0 ? '+' : '-';
|
|
||||||
const saldoPeriodoFormatado = `${sinalPeriodo}${horasPeriodo}h ${minutosPeriodo}min`;
|
|
||||||
|
|
||||||
// Calcular total de dias do período selecionado
|
// Calcular médias diárias
|
||||||
const diasPeriodo = gerarDiasPeriodo(dataInicio, dataFim);
|
const mediaDiariaTrabalhadaHoras = totalDiasPeriodo > 0 ? Math.floor(saldoPeriodoTrabalhadoMinutos / 60 / totalDiasPeriodo) : 0;
|
||||||
const totalDiasPeriodo = diasPeriodo.length;
|
const mediaDiariaTrabalhadaMinutos = totalDiasPeriodo > 0 ? Math.floor((saldoPeriodoTrabalhadoMinutos / totalDiasPeriodo) % 60) : 0;
|
||||||
|
|
||||||
|
// Calcular média esperada baseada na configuração padrão (não no período)
|
||||||
|
const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number);
|
||||||
|
const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco.split(':').map(Number);
|
||||||
|
const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco.split(':').map(Number);
|
||||||
|
const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number);
|
||||||
|
|
||||||
|
// Par 1: entrada -> saida_almoco
|
||||||
|
const minutosPar1Esperado = (horaSaidaAlmoco * 60 + minutoSaidaAlmoco) - (horaEntrada * 60 + minutoEntrada);
|
||||||
|
const minutosPar1EsperadoAjustado = minutosPar1Esperado < 0 ? minutosPar1Esperado + 24 * 60 : minutosPar1Esperado;
|
||||||
|
|
||||||
|
// Par 2: retorno_almoco -> saida
|
||||||
|
const minutosPar2Esperado = (horaSaida * 60 + minutoSaida) - (horaRetornoAlmoco * 60 + minutoRetornoAlmoco);
|
||||||
|
const minutosPar2EsperadoAjustado = minutosPar2Esperado < 0 ? minutosPar2Esperado + 24 * 60 : minutosPar2Esperado;
|
||||||
|
|
||||||
|
const totalEsperadoDiarioMinutos = minutosPar1EsperadoAjustado + minutosPar2EsperadoAjustado;
|
||||||
|
const mediaDiariaEsperadaHoras = Math.floor(totalEsperadoDiarioMinutos / 60);
|
||||||
|
const mediaDiariaEsperadaMinutos = totalEsperadoDiarioMinutos % 60;
|
||||||
|
|
||||||
|
// Formatar valores
|
||||||
|
const horasPeriodoDiferenca = Math.floor(Math.abs(saldoPeriodoDiferencaMinutos) / 60);
|
||||||
|
const minutosPeriodoDiferenca = Math.abs(saldoPeriodoDiferencaMinutos) % 60;
|
||||||
|
const sinalPeriodoDiferenca = saldoPeriodoDiferencaMinutos >= 0 ? '+' : '-';
|
||||||
|
const saldoPeriodoDiferencaFormatado = `${sinalPeriodoDiferenca}${horasPeriodoDiferenca}h ${minutosPeriodoDiferenca}min`;
|
||||||
|
|
||||||
|
const horasPeriodoTrabalhado = Math.floor(saldoPeriodoTrabalhadoMinutos / 60);
|
||||||
|
const minutosPeriodoTrabalhado = saldoPeriodoTrabalhadoMinutos % 60;
|
||||||
|
const saldoPeriodoTrabalhadoFormatado = `+${horasPeriodoTrabalhado}h ${minutosPeriodoTrabalhado}min`;
|
||||||
|
|
||||||
|
const horasPeriodoEsperado = Math.floor(saldoPeriodoEsperadoMinutos / 60);
|
||||||
|
const minutosPeriodoEsperado = saldoPeriodoEsperadoMinutos % 60;
|
||||||
|
const saldoPeriodoEsperadoFormatado = `+${horasPeriodoEsperado}h ${minutosPeriodoEsperado}min`;
|
||||||
|
|
||||||
doc.setFontSize(12);
|
doc.setFontSize(12);
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
@@ -1324,20 +1821,45 @@
|
|||||||
const sinal = saldoMinutos >= 0 ? '+' : '-';
|
const sinal = saldoMinutos >= 0 ? '+' : '-';
|
||||||
const saldoFormatado = `${sinal}${horas}h ${minutos}min`;
|
const saldoFormatado = `${sinal}${horas}h ${minutos}min`;
|
||||||
|
|
||||||
// Preparar dados da tabela
|
// Calcular saldo acumulado de períodos anteriores
|
||||||
const bancoHorasData: string[][] = [
|
// Saldo Anterior = Saldo Total - Saldo do Período Atual
|
||||||
|
const saldoAnteriorMinutos = saldoMinutos - saldoPeriodoDiferencaMinutos;
|
||||||
|
const horasAnterior = Math.floor(Math.abs(saldoAnteriorMinutos) / 60);
|
||||||
|
const minutosAnterior = Math.abs(saldoAnteriorMinutos) % 60;
|
||||||
|
const sinalAnterior = saldoAnteriorMinutos >= 0 ? '+' : '-';
|
||||||
|
const saldoAnteriorFormatado = `${sinalAnterior}${horasAnterior}h ${minutosAnterior}min`;
|
||||||
|
|
||||||
|
// Calcular resultado final
|
||||||
|
const resultadoFinalMinutos = saldoAnteriorMinutos + saldoPeriodoDiferencaMinutos;
|
||||||
|
const horasResultadoFinal = Math.floor(Math.abs(resultadoFinalMinutos) / 60);
|
||||||
|
const minutosResultadoFinal = Math.abs(resultadoFinalMinutos) % 60;
|
||||||
|
const sinalResultadoFinal = resultadoFinalMinutos >= 0 ? '+' : '-';
|
||||||
|
const resultadoFinalFormatado = `${sinalResultadoFinal}${horasResultadoFinal}h ${minutosResultadoFinal}min`;
|
||||||
|
|
||||||
|
// Preparar dados da tabela com melhorias
|
||||||
|
const bancoHorasData: any[][] = [
|
||||||
['Saldo Atual', saldoFormatado],
|
['Saldo Atual', saldoFormatado],
|
||||||
['Saldo do Período Selecionado', saldoPeriodoFormatado]
|
['Saldo Banco Acumulado de Períodos Anteriores', saldoAnteriorFormatado],
|
||||||
|
['Saldo do Período Exibido', saldoPeriodoDiferencaFormatado],
|
||||||
|
['Resultado Final', resultadoFinalFormatado]
|
||||||
];
|
];
|
||||||
|
|
||||||
if (saldoMinutos > 0) {
|
// Adicionar detalhamento
|
||||||
bancoHorasData.push(['Horas Excedentes', `${horas}h ${minutos}min`]);
|
bancoHorasData.push(['', '']); // Linha separadora
|
||||||
}
|
bancoHorasData.push(['Saldo Trabalhado do Período', saldoPeriodoTrabalhadoFormatado]);
|
||||||
|
bancoHorasData.push(['Saldo Esperado do Período', saldoPeriodoEsperadoFormatado]);
|
||||||
|
bancoHorasData.push(['Diferença do Período', saldoPeriodoDiferencaFormatado]);
|
||||||
|
|
||||||
if (saldoMinutos < 0) {
|
// Adicionar estatísticas
|
||||||
bancoHorasData.push(['Horas a Pagar', `${horas}h ${minutos}min`]);
|
bancoHorasData.push(['', '']); // Linha separadora
|
||||||
}
|
bancoHorasData.push(['Média Diária de Horas Trabalhadas', `+${mediaDiariaTrabalhadaHoras}h ${mediaDiariaTrabalhadaMinutos}min`]);
|
||||||
|
bancoHorasData.push(['Média Diária Esperada', `+${mediaDiariaEsperadaHoras}h ${mediaDiariaEsperadaMinutos}min`]);
|
||||||
|
|
||||||
|
// Adicionar contagens
|
||||||
|
bancoHorasData.push(['', '']); // Linha separadora
|
||||||
|
bancoHorasData.push(['Dias com Saldo Positivo', `${diasComSaldoPositivo} dias`]);
|
||||||
|
bancoHorasData.push(['Dias com Saldo Negativo', `${diasComSaldoNegativo} dias`]);
|
||||||
|
bancoHorasData.push(['Dias sem Registros', `${diasSemRegistros} dias`]);
|
||||||
bancoHorasData.push(['Total de Dias do Período', `${totalDiasPeriodo} dias`]);
|
bancoHorasData.push(['Total de Dias do Período', `${totalDiasPeriodo} dias`]);
|
||||||
|
|
||||||
// Criar tabela no mesmo estilo das outras seções
|
// Criar tabela no mesmo estilo das outras seções
|
||||||
@@ -1353,18 +1875,69 @@
|
|||||||
1: { cellWidth: 'auto' }
|
1: { cellWidth: 'auto' }
|
||||||
},
|
},
|
||||||
didParseCell: function(data) {
|
didParseCell: function(data) {
|
||||||
|
// Ignorar linhas separadoras vazias
|
||||||
|
if (data.cell.text[0] === '' && data.column.index === 0) {
|
||||||
|
data.cell.styles.fillColor = [240, 240, 240]; // Cor de fundo cinza claro
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const campo = data.cell.text[0];
|
const campo = data.cell.text[0];
|
||||||
// Destacar "Saldo do Período Selecionado" em vermelho se negativo
|
const valor = data.cell.text[1] || '';
|
||||||
if (campo === 'Saldo do Período Selecionado' && saldoPeriodoMinutos < 0) {
|
|
||||||
data.cell.styles.textColor = [200, 0, 0];
|
// Aplicar cores baseado no valor
|
||||||
if (data.column.index === 1) {
|
if (data.column.index === 1 && valor) {
|
||||||
|
// Aplicar cor no Saldo Atual (vermelho para negativo, azul para positivo)
|
||||||
|
if (campo === 'Saldo Atual') {
|
||||||
|
if (saldoMinutos < 0) {
|
||||||
|
data.cell.styles.textColor = [200, 0, 0]; // Vermelho para negativo
|
||||||
|
} else if (saldoMinutos > 0) {
|
||||||
|
data.cell.styles.textColor = [0, 100, 200]; // Azul para positivo
|
||||||
|
}
|
||||||
data.cell.styles.fontStyle = 'bold';
|
data.cell.styles.fontStyle = 'bold';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Destacar "Horas a Pagar" em vermelho se saldo negativo
|
// Verificar se o valor contém sinal negativo
|
||||||
if (campo === 'Horas a Pagar' && saldoMinutos < 0) {
|
if (valor.includes('-') && !valor.includes('±')) {
|
||||||
data.cell.styles.textColor = [200, 0, 0];
|
data.cell.styles.textColor = [200, 0, 0]; // Vermelho para negativo
|
||||||
if (data.column.index === 1) {
|
if (campo === 'Saldo do Período Exibido' || campo === 'Diferença do Período' || campo === 'Resultado Final') {
|
||||||
|
data.cell.styles.fontStyle = 'bold';
|
||||||
|
}
|
||||||
|
} else if (valor.includes('+') || campo.includes('Trabalhado') || campo.includes('Esperado')) {
|
||||||
|
// Verde para valores positivos ou campos de trabalhado/esperado
|
||||||
|
if (campo.includes('Diferença') && saldoPeriodoDiferencaMinutos < 0) {
|
||||||
|
data.cell.styles.textColor = [200, 0, 0]; // Vermelho se diferença negativa
|
||||||
|
data.cell.styles.fontStyle = 'bold';
|
||||||
|
} else if (campo.includes('Diferença') && saldoPeriodoDiferencaMinutos > 0) {
|
||||||
|
data.cell.styles.textColor = [0, 128, 0]; // Verde se diferença positiva
|
||||||
|
data.cell.styles.fontStyle = 'bold';
|
||||||
|
} else if (campo.includes('Trabalhado') || campo.includes('Esperado')) {
|
||||||
|
data.cell.styles.textColor = [0, 128, 0]; // Verde para trabalhado/esperado
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destacar campos específicos
|
||||||
|
if (campo === 'Dias com Saldo Negativo') {
|
||||||
|
data.cell.styles.textColor = [200, 0, 0];
|
||||||
|
data.cell.styles.fontStyle = 'bold';
|
||||||
|
}
|
||||||
|
if (campo === 'Dias com Saldo Positivo') {
|
||||||
|
data.cell.styles.textColor = [0, 128, 0];
|
||||||
|
data.cell.styles.fontStyle = 'bold';
|
||||||
|
}
|
||||||
|
if (campo === 'Resultado Final') {
|
||||||
|
if (resultadoFinalMinutos < 0) {
|
||||||
|
data.cell.styles.textColor = [200, 0, 0];
|
||||||
|
} else if (resultadoFinalMinutos > 0) {
|
||||||
|
data.cell.styles.textColor = [0, 128, 0];
|
||||||
|
}
|
||||||
|
data.cell.styles.fontStyle = 'bold';
|
||||||
|
}
|
||||||
|
if (campo === 'Saldo do Período Exibido') {
|
||||||
|
if (saldoPeriodoDiferencaMinutos < 0) {
|
||||||
|
data.cell.styles.textColor = [200, 0, 0];
|
||||||
|
} else if (saldoPeriodoDiferencaMinutos > 0) {
|
||||||
|
data.cell.styles.textColor = [0, 128, 0];
|
||||||
|
}
|
||||||
data.cell.styles.fontStyle = 'bold';
|
data.cell.styles.fontStyle = 'bold';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user