feat: enhance registro-pontos page with date range handling and expected record generation
- Introduced functions to generate all dates within a selected period and to create expected records based on user configuration. - Updated the logic to process daily records, combining real and expected entries, and calculating daily balances. - Enhanced table rendering to visually distinguish between marked and unmarked records, improving user experience and clarity in time management.
This commit is contained in:
@@ -596,6 +596,55 @@
|
||||
return saldos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera array de todas as datas do período selecionado
|
||||
*/
|
||||
function gerarDiasPeriodo(dataInicio: string, dataFim: string): string[] {
|
||||
const dias: string[] = [];
|
||||
const inicio = new Date(dataInicio);
|
||||
const fim = new Date(dataFim);
|
||||
|
||||
for (let d = new Date(inicio); d <= fim; d.setDate(d.getDate() + 1)) {
|
||||
dias.push(d.toISOString().split('T')[0]!);
|
||||
}
|
||||
|
||||
return dias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera registros esperados para um dia baseado na configuração
|
||||
*/
|
||||
function gerarRegistrosEsperados(data: string, config: {
|
||||
horarioEntrada: string;
|
||||
horarioSaidaAlmoco: string;
|
||||
horarioRetornoAlmoco: string;
|
||||
horarioSaida: string;
|
||||
}): Array<{ tipo: string; hora: number; minuto: number; data: string }> {
|
||||
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);
|
||||
|
||||
return [
|
||||
{ tipo: 'entrada', hora: horaEntrada, minuto: minutoEntrada, data },
|
||||
{ tipo: 'saida_almoco', hora: horaSaidaAlmoco, minuto: minutoSaidaAlmoco, data },
|
||||
{ tipo: 'retorno_almoco', hora: horaRetornoAlmoco, minuto: minutoRetornoAlmoco, data },
|
||||
{ tipo: 'saida', hora: horaSaida, minuto: minutoSaida, data },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se um registro esperado foi marcado
|
||||
*/
|
||||
function registroFoiMarcado(
|
||||
registroEsperado: { tipo: string; hora: number; minuto: number },
|
||||
registrosReais: Array<{ tipo: string; hora: number; minuto: number }>
|
||||
): boolean {
|
||||
return registrosReais.some(
|
||||
(r) => r.tipo === registroEsperado.tipo
|
||||
);
|
||||
}
|
||||
|
||||
function abrirModalImpressao(funcionarioId: Id<'funcionarios'>) {
|
||||
funcionarioParaImprimir = funcionarioId;
|
||||
mostrarModalImpressao = true;
|
||||
@@ -840,12 +889,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Variável para armazenar saldos diários (usada no resumo do banco de horas)
|
||||
const saldosDiariosPorData: Record<string, number> = {};
|
||||
|
||||
// Tabela de registros
|
||||
if (sections.registrosPonto) {
|
||||
const config = await client.query(api.configuracaoPonto.obterConfiguracao, {});
|
||||
const tableData: string[][] = [];
|
||||
if (!config) {
|
||||
throw new Error('Configuração de ponto não encontrada');
|
||||
}
|
||||
|
||||
// Agrupar por data para incluir saldo diário
|
||||
const tableData: any[][] = [];
|
||||
|
||||
// Agrupar registros reais por data
|
||||
const registrosPorData: Record<
|
||||
string,
|
||||
Array<{
|
||||
@@ -885,24 +941,152 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Gerar todos os dias do período
|
||||
const diasPeriodo = gerarDiasPeriodo(dataInicio, dataFim);
|
||||
|
||||
// Ordenar registros por hora e minuto para garantir ordem correta
|
||||
const regsOrdenados = [...regs].sort((a, b) => {
|
||||
// Processar cada dia do período
|
||||
for (const data of diasPeriodo) {
|
||||
const dataFormatada = formatarDataDDMMAAAA(data);
|
||||
const regsReais = registrosPorData[data] || [];
|
||||
const regsEsperados = gerarRegistrosEsperados(data, config);
|
||||
|
||||
// Combinar registros reais e esperados
|
||||
const todosRegistros: Array<{
|
||||
tipo: string;
|
||||
hora: number;
|
||||
minuto: number;
|
||||
real: boolean;
|
||||
dentroDoPrazo?: boolean;
|
||||
}> = [];
|
||||
|
||||
// Adicionar registros reais
|
||||
for (const reg of regsReais) {
|
||||
todosRegistros.push({
|
||||
tipo: reg.tipo,
|
||||
hora: reg.hora,
|
||||
minuto: reg.minuto,
|
||||
real: true,
|
||||
dentroDoPrazo: reg.dentroDoPrazo,
|
||||
});
|
||||
}
|
||||
|
||||
// Adicionar registros esperados não marcados (em vermelho)
|
||||
for (const regEsperado of regsEsperados) {
|
||||
if (!registroFoiMarcado(regEsperado, regsReais)) {
|
||||
todosRegistros.push({
|
||||
tipo: regEsperado.tipo,
|
||||
hora: regEsperado.hora,
|
||||
minuto: regEsperado.minuto,
|
||||
real: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ordenar todos os registros por hora e minuto
|
||||
todosRegistros.sort((a, b) => {
|
||||
if (a.hora !== b.hora) {
|
||||
return a.hora - b.hora;
|
||||
}
|
||||
return a.minuto - b.minuto;
|
||||
});
|
||||
|
||||
// Calcular saldos por par entrada/saída
|
||||
const saldosPorPar = calcularSaldosPorPar(regsOrdenados);
|
||||
// Calcular saldos por par entrada/saída (apenas com registros reais)
|
||||
const regsReaisOrdenados = [...regsReais].sort((a, b) => {
|
||||
if (a.hora !== b.hora) return a.hora - b.hora;
|
||||
return a.minuto - b.minuto;
|
||||
});
|
||||
const saldosPorPar = calcularSaldosPorPar(regsReaisOrdenados);
|
||||
|
||||
for (let i = 0; i < regsOrdenados.length; i++) {
|
||||
const reg = regsOrdenados[i];
|
||||
// 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();
|
||||
|
||||
// Identificar pares incompletos e calcular saldos esperados
|
||||
for (let i = 0; i < todosRegistros.length; i++) {
|
||||
const reg = todosRegistros[i];
|
||||
|
||||
// Se é entrada ou retorno_almoco (início de par) e é real
|
||||
if ((reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco') && reg.real) {
|
||||
// Verificar se há saída correspondente real
|
||||
const tipoSaidaEsperado = reg.tipo === 'entrada' ? 'saida_almoco' : 'saida';
|
||||
const saidaEncontrada = regsReais.find(r => r.tipo === tipoSaidaEsperado);
|
||||
|
||||
if (!saidaEncontrada) {
|
||||
// Par incompleto: calcular saldo esperado baseado na configuração
|
||||
const regEsperado = regsEsperados.find(r => r.tipo === tipoSaidaEsperado);
|
||||
if (regEsperado) {
|
||||
const minutosEntrada = reg.hora * 60 + reg.minuto;
|
||||
const minutosSaidaEsperada = regEsperado.hora * 60 + regEsperado.minuto;
|
||||
let saldoMinutos = minutosSaidaEsperada - minutosEntrada;
|
||||
if (saldoMinutos < 0) {
|
||||
saldoMinutos += 24 * 60;
|
||||
}
|
||||
|
||||
const horas = Math.floor(saldoMinutos / 60);
|
||||
const minutos = saldoMinutos % 60;
|
||||
|
||||
// Contar quantos registros fazem parte deste par na lista todosRegistros
|
||||
// Encontrar índice do registro na lista ordenada todosRegistros
|
||||
const indexNaListaTodos = todosRegistros.findIndex(
|
||||
(r, idx) => idx >= i && r.tipo === reg.tipo && r.hora === reg.hora && r.minuto === reg.minuto
|
||||
);
|
||||
|
||||
// Contar registros do par (entrada + saída esperada)
|
||||
let tamanhoPar = 1; // entrada
|
||||
const saidaEsperadaNaLista = todosRegistros.find(
|
||||
(r, idx) => idx > indexNaListaTodos && r.tipo === tipoSaidaEsperado && !r.real
|
||||
);
|
||||
if (saidaEsperadaNaLista) {
|
||||
tamanhoPar++; // saída faltante já está na lista
|
||||
}
|
||||
|
||||
if (indexNaListaTodos >= 0) {
|
||||
saldosEsperadosPorPar.set(indexNaListaTodos, {
|
||||
saldoMinutos,
|
||||
horas,
|
||||
minutos,
|
||||
tamanhoPar,
|
||||
incompleto: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calcular saldo diário total (soma de todos os pares reais)
|
||||
let saldoDiarioTotalMinutos = 0;
|
||||
for (const saldo of saldosPorPar.values()) {
|
||||
saldoDiarioTotalMinutos += saldo.saldoMinutos;
|
||||
}
|
||||
|
||||
// Se não há registros reais, calcular saldo esperado baseado na configuração
|
||||
if (regsReais.length === 0) {
|
||||
// Calcular saldo esperado do dia completo
|
||||
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 minutosPar1Entrada = horaEntrada * 60 + minutoEntrada;
|
||||
const minutosPar1Saida = horaSaidaAlmoco * 60 + minutoSaidaAlmoco;
|
||||
let saldoPar1 = minutosPar1Saida - minutosPar1Entrada;
|
||||
if (saldoPar1 < 0) saldoPar1 += 24 * 60;
|
||||
|
||||
// Par 2: retorno_almoco -> saida
|
||||
const minutosPar2Entrada = horaRetornoAlmoco * 60 + minutoRetornoAlmoco;
|
||||
const minutosPar2Saida = horaSaida * 60 + minutoSaida;
|
||||
let saldoPar2 = minutosPar2Saida - minutosPar2Entrada;
|
||||
if (saldoPar2 < 0) saldoPar2 += 24 * 60;
|
||||
|
||||
saldoDiarioTotalMinutos = saldoPar1 + saldoPar2;
|
||||
}
|
||||
|
||||
saldosDiariosPorData[data] = saldoDiarioTotalMinutos;
|
||||
|
||||
// Criar linhas da tabela
|
||||
for (let i = 0; i < todosRegistros.length; i++) {
|
||||
const reg = todosRegistros[i];
|
||||
const linha: any[] = [
|
||||
dataFormatada,
|
||||
config
|
||||
@@ -916,28 +1100,110 @@
|
||||
formatarHoraPonto(reg.hora, reg.minuto),
|
||||
];
|
||||
|
||||
// Marcar linha como não marcada para aplicar cor vermelha depois
|
||||
if (!reg.real) {
|
||||
linha._naoMarcado = true;
|
||||
}
|
||||
|
||||
// Saldo Diário por par entrada/saída
|
||||
if (sections.saldoDiario) {
|
||||
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 (reg.real && isInicioPar) {
|
||||
const indexReal = regsReaisOrdenados.findIndex(
|
||||
(r) => r.tipo === reg.tipo && r.hora === reg.hora && r.minuto === reg.minuto
|
||||
);
|
||||
|
||||
// Verificar se há saldo esperado (par incompleto)
|
||||
const saldoEsperado = saldosEsperadosPorPar.get(i);
|
||||
if (saldoEsperado) {
|
||||
// Par incompleto: mostrar saldo esperado em vermelho
|
||||
linha._saldoVermelho = true;
|
||||
linha.push({
|
||||
content: `+${saldoEsperado.horas}h ${saldoEsperado.minutos}min`,
|
||||
rowSpan: saldoEsperado.tamanhoPar
|
||||
});
|
||||
} else if (indexReal >= 0) {
|
||||
const saldoPar = saldosPorPar.get(indexReal);
|
||||
if (saldoPar) {
|
||||
linha.push({
|
||||
content: `+${saldoPar.horas}h ${saldoPar.minutos}min`,
|
||||
rowSpan: saldoPar.tamanhoPar
|
||||
});
|
||||
} else {
|
||||
linha.push('-');
|
||||
}
|
||||
} else {
|
||||
linha.push('-');
|
||||
}
|
||||
} else if (reg.real) {
|
||||
// Saída real: não adicionar (já coberto pelo rowspan da entrada)
|
||||
// Mas verificar se precisa adicionar '-' se não houver par completo
|
||||
const tipoEntradaEsperado = reg.tipo === 'saida_almoco' ? 'entrada' : 'retorno_almoco';
|
||||
const entradaReal = regsReais.find(r => r.tipo === tipoEntradaEsperado);
|
||||
if (!entradaReal) {
|
||||
linha.push('-');
|
||||
}
|
||||
} else {
|
||||
// Registro não marcado: verificar se faz parte de um par incompleto ou dia sem registros
|
||||
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
|
||||
});
|
||||
// Verificar se há saldo esperado para este par
|
||||
const saldoEsperado = saldosEsperadosPorPar.get(i);
|
||||
if (saldoEsperado) {
|
||||
// Par incompleto: mostrar saldo esperado em vermelho
|
||||
linha._saldoVermelho = true;
|
||||
linha.push({
|
||||
content: `+${saldoEsperado.horas}h ${saldoEsperado.minutos}min`,
|
||||
rowSpan: saldoEsperado.tamanhoPar
|
||||
});
|
||||
} else if (regsReais.length === 0) {
|
||||
// Dia sem registros: calcular saldo esperado completo
|
||||
if (reg.tipo === 'entrada') {
|
||||
// Par 1 completo esperado
|
||||
const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number);
|
||||
const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco.split(':').map(Number);
|
||||
const minutosEntrada = horaEntrada * 60 + minutoEntrada;
|
||||
const minutosSaida = horaSaidaAlmoco * 60 + minutoSaidaAlmoco;
|
||||
let saldoMinutos = minutosSaida - minutosEntrada;
|
||||
if (saldoMinutos < 0) saldoMinutos += 24 * 60;
|
||||
const horas = Math.floor(saldoMinutos / 60);
|
||||
const minutos = saldoMinutos % 60;
|
||||
linha._saldoVermelho = true;
|
||||
linha.push({
|
||||
content: `+${horas}h ${minutos}min`,
|
||||
rowSpan: 2 // entrada + saida_almoco
|
||||
});
|
||||
} else if (reg.tipo === 'retorno_almoco') {
|
||||
// Par 2 completo esperado
|
||||
const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco.split(':').map(Number);
|
||||
const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number);
|
||||
const minutosEntrada = horaRetornoAlmoco * 60 + minutoRetornoAlmoco;
|
||||
const minutosSaida = horaSaida * 60 + minutoSaida;
|
||||
let saldoMinutos = minutosSaida - minutosEntrada;
|
||||
if (saldoMinutos < 0) saldoMinutos += 24 * 60;
|
||||
const horas = Math.floor(saldoMinutos / 60);
|
||||
const minutos = saldoMinutos % 60;
|
||||
linha._saldoVermelho = true;
|
||||
linha.push({
|
||||
content: `+${horas}h ${minutos}min`,
|
||||
rowSpan: 2 // retorno_almoco + saida
|
||||
});
|
||||
} else {
|
||||
linha.push('-');
|
||||
}
|
||||
} else {
|
||||
linha.push('-');
|
||||
}
|
||||
} else {
|
||||
// Saída sem entrada correspondente: não tem saldo
|
||||
linha.push('-');
|
||||
}
|
||||
// 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('-');
|
||||
}
|
||||
}
|
||||
|
||||
linha.push(reg.dentroDoPrazo ? 'Sim' : 'Não');
|
||||
linha.push(reg.real ? (reg.dentroDoPrazo ? 'Sim' : 'Não') : 'Não marcado');
|
||||
|
||||
tableData.push(linha);
|
||||
}
|
||||
@@ -959,6 +1225,23 @@
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
didParseCell: function(data) {
|
||||
// Aplicar cor vermelha para registros não marcados
|
||||
if (data.row.raw && (data.row.raw as any)._naoMarcado) {
|
||||
// Aplicar cor vermelha nas colunas Tipo e Horário
|
||||
if (data.column.index === 1 || data.column.index === 2) {
|
||||
data.cell.styles.textColor = [200, 0, 0];
|
||||
}
|
||||
}
|
||||
// Aplicar cor vermelha na coluna de saldo diário quando marcado
|
||||
if (data.row.raw && (data.row.raw as any)._saldoVermelho) {
|
||||
// Coluna de saldo diário (índice 3 se saldoDiario estiver ativo)
|
||||
const indiceSaldoDiario = sections.saldoDiario ? 3 : -1;
|
||||
if (data.column.index === indiceSaldoDiario) {
|
||||
data.cell.styles.textColor = [200, 0, 0];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Calcular posição Y após a tabela
|
||||
@@ -986,27 +1269,34 @@
|
||||
funcionarioId,
|
||||
});
|
||||
|
||||
// Calcular saldo do período selecionado
|
||||
// Calcular saldo do período selecionado baseado nos saldos diários calculados
|
||||
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] = [];
|
||||
if (sections.registrosPonto && Object.keys(saldosDiariosPorData).length > 0) {
|
||||
// Somar todos os saldos diários do período
|
||||
for (const saldoMinutos of Object.values(saldosDiariosPorData)) {
|
||||
saldoPeriodoMinutos += saldoMinutos;
|
||||
}
|
||||
registrosPorDataPeriodo[dataKey]!.push({
|
||||
tipo: r.tipo,
|
||||
hora: r.hora,
|
||||
minuto: r.minuto,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Fallback: calcular a partir dos registros se não tiver saldos diários
|
||||
const registrosPorDataPeriodo: Record<string, Array<{ tipo: string; hora: number; minuto: number }>> = {};
|
||||
|
||||
// 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;
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
for (const regs of Object.values(registrosPorDataPeriodo)) {
|
||||
const saldoDiario = calcularSaldoDiario(regs);
|
||||
if (saldoDiario) {
|
||||
saldoPeriodoMinutos += saldoDiario.saldoMinutos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1015,6 +1305,10 @@
|
||||
const sinalPeriodo = saldoPeriodoMinutos >= 0 ? '+' : '-';
|
||||
const saldoPeriodoFormatado = `${sinalPeriodo}${horasPeriodo}h ${minutosPeriodo}min`;
|
||||
|
||||
// Calcular total de dias do período selecionado
|
||||
const diasPeriodo = gerarDiasPeriodo(dataInicio, dataFim);
|
||||
const totalDiasPeriodo = diasPeriodo.length;
|
||||
|
||||
doc.setFontSize(12);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(41, 128, 185);
|
||||
@@ -1044,7 +1338,7 @@
|
||||
bancoHorasData.push(['Horas a Pagar', `${horas}h ${minutos}min`]);
|
||||
}
|
||||
|
||||
bancoHorasData.push(['Total de Dias com Registro', `${bancoHoras.totalDias} dias`]);
|
||||
bancoHorasData.push(['Total de Dias do Período', `${totalDiasPeriodo} dias`]);
|
||||
|
||||
// Criar tabela no mesmo estilo das outras seções
|
||||
autoTable(doc, {
|
||||
|
||||
Reference in New Issue
Block a user