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'>) {
|
function abrirModalImpressao(funcionarioId: Id<'funcionarios'>) {
|
||||||
funcionarioParaImprimir = funcionarioId;
|
funcionarioParaImprimir = funcionarioId;
|
||||||
mostrarModalImpressao = true;
|
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)) {
|
for (const [data, regs] of Object.entries(registrosPorData)) {
|
||||||
// Formatar data para exibição usando função centralizada (DD/MM/AAAA)
|
// Formatar data para exibição usando função centralizada (DD/MM/AAAA)
|
||||||
const dataFormatada = formatarDataDDMMAAAA(data);
|
const dataFormatada = formatarDataDDMMAAAA(data);
|
||||||
|
|
||||||
// Calcular saldo diário como diferença entre saída e entrada
|
// Ordenar registros por hora e minuto para garantir ordem correta
|
||||||
const saldoDiarioDia = calcularSaldoDiario(regs);
|
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) {
|
// Calcular saldos por par entrada/saída
|
||||||
const linha: string[] = [
|
const saldosPorPar = calcularSaldosPorPar(regsOrdenados);
|
||||||
|
|
||||||
|
for (let i = 0; i < regsOrdenados.length; i++) {
|
||||||
|
const reg = regsOrdenados[i];
|
||||||
|
const linha: any[] = [
|
||||||
dataFormatada,
|
dataFormatada,
|
||||||
config
|
config
|
||||||
? getTipoRegistroLabel(reg.tipo as 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida', {
|
? getTipoRegistroLabel(reg.tipo as 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida', {
|
||||||
@@ -838,42 +916,29 @@
|
|||||||
formatarHoraPonto(reg.hora, reg.minuto),
|
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 (sections.saldoDiario) {
|
||||||
if (saldoDiarioDia) {
|
const saldoPar = saldosPorPar.get(i);
|
||||||
const sinal = saldoDiarioDia.positivo ? '+' : '-';
|
if (saldoPar) {
|
||||||
linha.push(`${sinal}${saldoDiarioDia.horas}h ${saldoDiarioDia.minutos}min`);
|
// 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 {
|
} else {
|
||||||
|
// Registro sem par completo: mostrar "-" em célula individual
|
||||||
linha.push('-');
|
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');
|
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);
|
tableData.push(linha);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -882,9 +947,7 @@
|
|||||||
if (sections.saldoDiario) {
|
if (sections.saldoDiario) {
|
||||||
headers.push('Saldo Diário');
|
headers.push('Saldo Diário');
|
||||||
}
|
}
|
||||||
headers.push('Localização');
|
|
||||||
headers.push('Dentro do Prazo');
|
headers.push('Dentro do Prazo');
|
||||||
headers.push('Acelerômetro');
|
|
||||||
|
|
||||||
// Salvar a posição Y antes da tabela
|
// Salvar a posição Y antes da tabela
|
||||||
const yPosAntesTabela = yPosition;
|
const yPosAntesTabela = yPosition;
|
||||||
@@ -923,15 +986,42 @@
|
|||||||
funcionarioId,
|
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.setFontSize(12);
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.setTextColor(41, 128, 185);
|
doc.setTextColor(41, 128, 185);
|
||||||
doc.text('RESUMO DO BANCO DE HORAS', 15, yPosition);
|
doc.text('RESUMO DO BANCO DE HORAS', 15, yPosition);
|
||||||
doc.setFont('helvetica', 'normal');
|
doc.setFont('helvetica', 'normal');
|
||||||
doc.setTextColor(0, 0, 0);
|
doc.setTextColor(0, 0, 0);
|
||||||
|
|
||||||
yPosition += 10;
|
yPosition += 10;
|
||||||
doc.setFontSize(10);
|
|
||||||
|
|
||||||
if (bancoHoras) {
|
if (bancoHoras) {
|
||||||
const saldoMinutos = bancoHoras.saldoAcumuladoMinutos;
|
const saldoMinutos = bancoHoras.saldoAcumuladoMinutos;
|
||||||
@@ -940,40 +1030,81 @@
|
|||||||
const sinal = saldoMinutos >= 0 ? '+' : '-';
|
const sinal = saldoMinutos >= 0 ? '+' : '-';
|
||||||
const saldoFormatado = `${sinal}${horas}h ${minutos}min`;
|
const saldoFormatado = `${sinal}${horas}h ${minutos}min`;
|
||||||
|
|
||||||
doc.setFont('helvetica', 'bold');
|
// Preparar dados da tabela
|
||||||
doc.text('Saldo Atual:', 15, yPosition);
|
const bancoHorasData: string[][] = [
|
||||||
doc.setFont('helvetica', 'normal');
|
['Saldo Atual', saldoFormatado],
|
||||||
doc.text(saldoFormatado, 60, yPosition);
|
['Saldo do Período Selecionado', saldoPeriodoFormatado]
|
||||||
yPosition += 8;
|
];
|
||||||
|
|
||||||
if (saldoMinutos > 0) {
|
if (saldoMinutos > 0) {
|
||||||
doc.setFont('helvetica', 'bold');
|
bancoHorasData.push(['Horas Excedentes', `${horas}h ${minutos}min`]);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saldoMinutos < 0) {
|
if (saldoMinutos < 0) {
|
||||||
doc.setFont('helvetica', 'bold');
|
bancoHorasData.push(['Horas a Pagar', `${horas}h ${minutos}min`]);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doc.setFont('helvetica', 'bold');
|
bancoHorasData.push(['Total de Dias com Registro', `${bancoHoras.totalDias} dias`]);
|
||||||
doc.text('Total de Dias com Registro:', 15, yPosition);
|
|
||||||
doc.setFont('helvetica', 'normal');
|
// Criar tabela no mesmo estilo das outras seções
|
||||||
doc.text(`${bancoHoras.totalDias} dias`, 95, yPosition);
|
autoTable(doc, {
|
||||||
yPosition += 10;
|
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 {
|
||||||
|
yPosition += bancoHorasData.length * 7 + 10;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
doc.text('Banco de horas não disponível', 15, yPosition);
|
// Se não houver banco de horas, criar tabela vazia com mensagem
|
||||||
yPosition += 10;
|
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