feat: implement saldo calculation for entry/exit pairs in registro-pontos page
- Added a new function `calcularSaldosPorPar` to compute balances for entry and exit pairs, returning a map of balances indexed by record. - Updated the table data generation to display daily balances per pair, enhancing visibility of time management. - Implemented logic to handle incomplete pairs and display appropriate messages in the UI, improving user experience.
This commit is contained in:
@@ -527,6 +527,75 @@
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula saldos por par entrada/saída
|
||||
* Retorna um mapa com o índice do registro e informações do saldo do par
|
||||
*/
|
||||
function calcularSaldosPorPar(registros: Array<{ tipo: string; hora: number; minuto: number }>): Map<number, { saldoMinutos: number; horas: number; minutos: number; parIndex: number; tamanhoPar: number }> {
|
||||
const saldos = new Map<number, { saldoMinutos: number; horas: number; minutos: number; parIndex: number; tamanhoPar: number }>();
|
||||
|
||||
if (registros.length === 0) return saldos;
|
||||
|
||||
// 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 saldo do par (saída - entrada)
|
||||
const minutosEntrada = entradaAtual.hora * 60 + entradaAtual.minuto;
|
||||
const minutosSaida = reg.hora * 60 + reg.minuto;
|
||||
|
||||
let saldoMinutos = minutosSaida - minutosEntrada;
|
||||
if (saldoMinutos < 0) {
|
||||
saldoMinutos += 24 * 60; // Adicionar um dia em minutos
|
||||
}
|
||||
|
||||
const horas = Math.floor(saldoMinutos / 60);
|
||||
const minutos = saldoMinutos % 60;
|
||||
|
||||
// Associar saldo a todos os registros do par
|
||||
for (const idx of indicesPar) {
|
||||
saldos.set(idx, {
|
||||
saldoMinutos,
|
||||
horas,
|
||||
minutos,
|
||||
parIndex,
|
||||
tamanhoPar: indicesPar.length
|
||||
});
|
||||
}
|
||||
|
||||
parIndex++;
|
||||
entradaAtual = null;
|
||||
indicesPar = [];
|
||||
}
|
||||
}
|
||||
|
||||
return saldos;
|
||||
}
|
||||
|
||||
function abrirModalImpressao(funcionarioId: Id<'funcionarios'>) {
|
||||
funcionarioParaImprimir = funcionarioId;
|
||||
mostrarModalImpressao = true;
|
||||
@@ -816,16 +885,25 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Criar dados da tabela com saldo diário
|
||||
// Criar dados da tabela com saldo diário por par
|
||||
for (const [data, regs] of Object.entries(registrosPorData)) {
|
||||
// Formatar data para exibição usando função centralizada (DD/MM/AAAA)
|
||||
const dataFormatada = formatarDataDDMMAAAA(data);
|
||||
|
||||
// Calcular saldo diário como diferença entre saída e entrada
|
||||
const saldoDiarioDia = calcularSaldoDiario(regs);
|
||||
// Ordenar registros por hora e minuto para garantir ordem correta
|
||||
const regsOrdenados = [...regs].sort((a, b) => {
|
||||
if (a.hora !== b.hora) {
|
||||
return a.hora - b.hora;
|
||||
}
|
||||
return a.minuto - b.minuto;
|
||||
});
|
||||
|
||||
for (const reg of regs) {
|
||||
const linha: string[] = [
|
||||
// Calcular saldos por par entrada/saída
|
||||
const saldosPorPar = calcularSaldosPorPar(regsOrdenados);
|
||||
|
||||
for (let i = 0; i < regsOrdenados.length; i++) {
|
||||
const reg = regsOrdenados[i];
|
||||
const linha: any[] = [
|
||||
dataFormatada,
|
||||
config
|
||||
? getTipoRegistroLabel(reg.tipo as 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida', {
|
||||
@@ -838,42 +916,29 @@
|
||||
formatarHoraPonto(reg.hora, reg.minuto),
|
||||
];
|
||||
|
||||
// Saldo Diário sempre após Horário
|
||||
// Saldo Diário por par entrada/saída
|
||||
if (sections.saldoDiario) {
|
||||
if (saldoDiarioDia) {
|
||||
const sinal = saldoDiarioDia.positivo ? '+' : '-';
|
||||
linha.push(`${sinal}${saldoDiarioDia.horas}h ${saldoDiarioDia.minutos}min`);
|
||||
const saldoPar = saldosPorPar.get(i);
|
||||
if (saldoPar) {
|
||||
// Verificar se é o primeiro registro do par (entrada ou retorno_almoco)
|
||||
const isInicioPar = reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco';
|
||||
|
||||
if (isInicioPar) {
|
||||
// Primeira linha do par: adicionar saldo com rowspan
|
||||
linha.push({
|
||||
content: `+${saldoPar.horas}h ${saldoPar.minutos}min`,
|
||||
rowSpan: saldoPar.tamanhoPar
|
||||
});
|
||||
}
|
||||
// Outras linhas do par (saida_almoco ou saida): não adicionar coluna (o rowspan cobre)
|
||||
} else {
|
||||
// Registro sem par completo: mostrar "-" em célula individual
|
||||
linha.push('-');
|
||||
}
|
||||
}
|
||||
|
||||
// Adicionar localização (geofencing)
|
||||
if (reg.dentroRaioPermitido === true) {
|
||||
linha.push('✅ Dentro do Raio');
|
||||
} else if (reg.dentroRaioPermitido === false) {
|
||||
linha.push('⚠️ Fora do Raio');
|
||||
} else {
|
||||
linha.push('❓ Não Validado');
|
||||
}
|
||||
|
||||
linha.push(reg.dentroDoPrazo ? 'Sim' : 'Não');
|
||||
|
||||
// Adicionar dados de acelerômetro (se disponível)
|
||||
if (reg.acelerometroX !== undefined || reg.sensorDisponivel !== undefined) {
|
||||
if (reg.sensorDisponivel === false && !reg.acelerometroX) {
|
||||
linha.push('Sensor: Não disponível');
|
||||
} else if (reg.acelerometroX !== undefined) {
|
||||
const movimento = reg.movimentoDetectado ? 'Sim' : 'Não';
|
||||
const magnitude = reg.magnitudeMovimento !== undefined ? reg.magnitudeMovimento.toFixed(2) : 'N/A';
|
||||
linha.push(`Mov: ${movimento} | Mag: ${magnitude} m/s²`);
|
||||
} else {
|
||||
linha.push('Sensor: N/A');
|
||||
}
|
||||
} else {
|
||||
linha.push('-');
|
||||
}
|
||||
|
||||
tableData.push(linha);
|
||||
}
|
||||
}
|
||||
@@ -882,9 +947,7 @@
|
||||
if (sections.saldoDiario) {
|
||||
headers.push('Saldo Diário');
|
||||
}
|
||||
headers.push('Localização');
|
||||
headers.push('Dentro do Prazo');
|
||||
headers.push('Acelerômetro');
|
||||
|
||||
// Salvar a posição Y antes da tabela
|
||||
const yPosAntesTabela = yPosition;
|
||||
@@ -923,15 +986,42 @@
|
||||
funcionarioId,
|
||||
});
|
||||
|
||||
// Calcular saldo do período selecionado
|
||||
let saldoPeriodoMinutos = 0;
|
||||
const registrosPorDataPeriodo: Record<string, Array<{ tipo: string; hora: number; minuto: number }>> = {};
|
||||
|
||||
for (const r of registrosFuncionario) {
|
||||
const dataKey = r.data;
|
||||
if (!registrosPorDataPeriodo[dataKey]) {
|
||||
registrosPorDataPeriodo[dataKey] = [];
|
||||
}
|
||||
registrosPorDataPeriodo[dataKey]!.push({
|
||||
tipo: r.tipo,
|
||||
hora: r.hora,
|
||||
minuto: r.minuto,
|
||||
});
|
||||
}
|
||||
|
||||
// Somar todos os saldos diários do período
|
||||
for (const regs of Object.values(registrosPorDataPeriodo)) {
|
||||
const saldoDiario = calcularSaldoDiario(regs);
|
||||
if (saldoDiario) {
|
||||
saldoPeriodoMinutos += saldoDiario.saldoMinutos;
|
||||
}
|
||||
}
|
||||
|
||||
const horasPeriodo = Math.floor(Math.abs(saldoPeriodoMinutos) / 60);
|
||||
const minutosPeriodo = Math.abs(saldoPeriodoMinutos) % 60;
|
||||
const sinalPeriodo = saldoPeriodoMinutos >= 0 ? '+' : '-';
|
||||
const saldoPeriodoFormatado = `${sinalPeriodo}${horasPeriodo}h ${minutosPeriodo}min`;
|
||||
|
||||
doc.setFontSize(12);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(41, 128, 185);
|
||||
doc.text('RESUMO DO BANCO DE HORAS', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(0, 0, 0);
|
||||
|
||||
yPosition += 10;
|
||||
doc.setFontSize(10);
|
||||
|
||||
if (bancoHoras) {
|
||||
const saldoMinutos = bancoHoras.saldoAcumuladoMinutos;
|
||||
@@ -940,40 +1030,81 @@
|
||||
const sinal = saldoMinutos >= 0 ? '+' : '-';
|
||||
const saldoFormatado = `${sinal}${horas}h ${minutos}min`;
|
||||
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Saldo Atual:', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(saldoFormatado, 60, yPosition);
|
||||
yPosition += 8;
|
||||
// Preparar dados da tabela
|
||||
const bancoHorasData: string[][] = [
|
||||
['Saldo Atual', saldoFormatado],
|
||||
['Saldo do Período Selecionado', saldoPeriodoFormatado]
|
||||
];
|
||||
|
||||
if (saldoMinutos > 0) {
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(0, 128, 0);
|
||||
doc.text('Horas Excedentes:', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`${horas}h ${minutos}min`, 75, yPosition);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
yPosition += 8;
|
||||
bancoHorasData.push(['Horas Excedentes', `${horas}h ${minutos}min`]);
|
||||
}
|
||||
|
||||
if (saldoMinutos < 0) {
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(200, 0, 0);
|
||||
doc.text('Horas a Pagar:', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`${horas}h ${minutos}min`, 70, yPosition);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
yPosition += 8;
|
||||
bancoHorasData.push(['Horas a Pagar', `${horas}h ${minutos}min`]);
|
||||
}
|
||||
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Total de Dias com Registro:', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`${bancoHoras.totalDias} dias`, 95, yPosition);
|
||||
yPosition += 10;
|
||||
bancoHorasData.push(['Total de Dias com Registro', `${bancoHoras.totalDias} dias`]);
|
||||
|
||||
// Criar tabela no mesmo estilo das outras seções
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['Campo', 'Valor']],
|
||||
body: bancoHorasData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
columnStyles: {
|
||||
0: { fontStyle: 'bold', cellWidth: 80 },
|
||||
1: { cellWidth: 'auto' }
|
||||
},
|
||||
didParseCell: function(data) {
|
||||
const campo = data.cell.text[0];
|
||||
// Destacar "Saldo do Período Selecionado" em vermelho se negativo
|
||||
if (campo === 'Saldo do Período Selecionado' && saldoPeriodoMinutos < 0) {
|
||||
data.cell.styles.textColor = [200, 0, 0];
|
||||
if (data.column.index === 1) {
|
||||
data.cell.styles.fontStyle = 'bold';
|
||||
}
|
||||
}
|
||||
// Destacar "Horas a Pagar" em vermelho se saldo negativo
|
||||
if (campo === 'Horas a Pagar' && saldoMinutos < 0) {
|
||||
data.cell.styles.textColor = [200, 0, 0];
|
||||
if (data.column.index === 1) {
|
||||
data.cell.styles.fontStyle = 'bold';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Calcular posição Y após a tabela
|
||||
const lastPage = doc.getNumberOfPages();
|
||||
doc.setPage(lastPage);
|
||||
const finalY = (doc as { lastAutoTable?: { finalY: number } }).lastAutoTable?.finalY;
|
||||
if (finalY) {
|
||||
yPosition = finalY + 10;
|
||||
} else {
|
||||
doc.text('Banco de horas não disponível', 15, yPosition);
|
||||
yPosition += 10;
|
||||
yPosition += bancoHorasData.length * 7 + 10;
|
||||
}
|
||||
} else {
|
||||
// Se não houver banco de horas, criar tabela vazia com mensagem
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['Campo', 'Valor']],
|
||||
body: [['Banco de horas', 'Não disponível']],
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
const lastPage = doc.getNumberOfPages();
|
||||
doc.setPage(lastPage);
|
||||
const finalY = (doc as { lastAutoTable?: { finalY: number } }).lastAutoTable?.finalY;
|
||||
if (finalY) {
|
||||
yPosition = finalY + 10;
|
||||
} else {
|
||||
yPosition += 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user