diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte index a03d425..687ed2a 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte @@ -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 { + const saldos = new Map(); + + 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> = {}; + + 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 { + yPosition += bancoHorasData.length * 7 + 10; + } } else { - doc.text('Banco de horas não disponível', 15, yPosition); - yPosition += 10; + // 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; + } } }