import type { ConvexClient } from 'convex-svelte'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import { api } from '@sgse-app/backend/convex/_generated/api'; import type { DiaFichaPonto, ResumoPeriodo, RegistroPonto, TipoDia } from './tipos'; import { calcularSaldoComparativoPorPar } from './calculos'; import { registroFoiMarcado } from './validacao'; import { formatarDataDDMMAAAA } from '../ponto'; import { formatarMinutos, formatarHoras } from './formatacao'; /** * Gera array de todas as datas do período selecionado */ export 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 */ export 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 } ]; } /** * Agrupa registros por funcionário e data */ export function agruparRegistrosPorFuncionario( registros: Array<{ _id: Id<'registrosPonto'>; funcionarioId: Id<'funcionarios'>; data: string; funcionario?: { nome: string; matricula?: string; descricaoCargo?: string } | null; [key: string]: any; }>, config?: { horarioEntrada: string; horarioSaidaAlmoco: string; horarioRetornoAlmoco: string; horarioSaida: string; } ): Array<{ funcionario: { nome: string; matricula?: string; descricaoCargo?: string } | null; funcionarioId: Id<'funcionarios'>; registrosPorData: Record< string, { data: string; registros: typeof registros; saldoDiario?: { saldoMinutos: number; horas: number; minutos: number; positivo: boolean; }; saldoDiarioComparativo?: { trabalhadoMinutos: number; esperadoMinutos: number; diferencaMinutos: number; }; } >; }> { const agrupados: Record< string, { funcionario: { nome: string; matricula?: string; descricaoCargo?: string } | null; funcionarioId: Id<'funcionarios'>; registrosPorData: Record< string, { data: string; registros: typeof registros; saldoDiario?: { saldoMinutos: number; horas: number; minutos: number; positivo: boolean; }; saldoDiarioComparativo?: { trabalhadoMinutos: number; esperadoMinutos: number; diferencaMinutos: number; }; } >; } > = {}; const registrosProcessados = new Set(); if (!Array.isArray(registros) || registros.length === 0) { return []; } for (const registro of registros) { if (!registro || !registro._id || !registro.funcionarioId || !registro.data) { continue; } const chaveUnica = `${registro._id}`; if (registrosProcessados.has(chaveUnica)) { continue; } registrosProcessados.add(chaveUnica); const key = registro.funcionarioId; if (!agrupados[key]) { agrupados[key] = { funcionario: registro.funcionario, funcionarioId: registro.funcionarioId, registrosPorData: {} }; } const dataKey = registro.data; if (!agrupados[key]!.registrosPorData[dataKey]) { agrupados[key]!.registrosPorData[dataKey] = { data: dataKey, registros: [], saldoDiario: undefined }; } const jaExiste = agrupados[key]!.registrosPorData[dataKey]!.registros.some( (r) => r._id === registro._id ); if (!jaExiste) { agrupados[key]!.registrosPorData[dataKey]!.registros.push(registro); } } const resultado = Object.values(agrupados); resultado.sort((a, b) => { const nomeA = a.funcionario?.nome || ''; const nomeB = b.funcionario?.nome || ''; return nomeA.localeCompare(nomeB, 'pt-BR'); }); for (const grupo of resultado) { const datasOrdenadas = Object.keys(grupo.registrosPorData).sort((a, b) => { return new Date(b).getTime() - new Date(a).getTime(); }); const registrosPorDataOrdenado: Record = {}; for (const dataKey of datasOrdenadas) { registrosPorDataOrdenado[dataKey] = grupo.registrosPorData[dataKey]!; } grupo.registrosPorData = registrosPorDataOrdenado; for (const dataKey in grupo.registrosPorData) { const grupoData = grupo.registrosPorData[dataKey]; if (grupoData && grupoData.registros.length > 0) { grupoData.registros.sort((a, b) => { if (a.hora !== b.hora) { return a.hora - b.hora; } return a.minuto - b.minuto; }); if (config) { const regsReaisOrdenados = [...grupoData.registros].sort((a, b) => { if (a.hora !== b.hora) return a.hora - b.hora; return a.minuto - b.minuto; }); const saldosComparativosPorPar = calcularSaldoComparativoPorPar(regsReaisOrdenados, config); let totalTrabalhado = 0; const paresProcessados = new Set(); for (const [, saldo] of saldosComparativosPorPar.entries()) { if (!paresProcessados.has(saldo.parIndex)) { totalTrabalhado += saldo.trabalhadoMinutos; paresProcessados.add(saldo.parIndex); } } 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); const minutosPar1EsperadoConfig = horaSaidaAlmocoConfig * 60 + minutoSaidaAlmocoConfig - (horaEntradaConfig * 60 + minutoEntradaConfig); const minutosPar1EsperadoAjustadoConfig = minutosPar1EsperadoConfig < 0 ? minutosPar1EsperadoConfig + 24 * 60 : minutosPar1EsperadoConfig; const minutosPar2EsperadoConfig = horaSaidaConfig * 60 + minutoSaidaConfig - (horaRetornoAlmocoConfig * 60 + minutoRetornoAlmocoConfig); const minutosPar2EsperadoAjustadoConfig = minutosPar2EsperadoConfig < 0 ? minutosPar2EsperadoConfig + 24 * 60 : minutosPar2EsperadoConfig; const cargaHorariaDiariaEsperadaMinutos = minutosPar1EsperadoAjustadoConfig + minutosPar2EsperadoAjustadoConfig; const diferencaMinutos = totalTrabalhado - cargaHorariaDiariaEsperadaMinutos; grupoData.saldoDiarioComparativo = { trabalhadoMinutos: totalTrabalhado, esperadoMinutos: cargaHorariaDiariaEsperadaMinutos, diferencaMinutos: diferencaMinutos }; } } } } return resultado.filter((grupo) => { const temRegistros = Object.values(grupo.registrosPorData).some( (grupoData) => grupoData.registros && grupoData.registros.length > 0 ); return temRegistros; }); } /** * Processa dados para ficha de ponto * Esta é uma função grande que processa todos os dados necessários para gerar a ficha */ export async function processarDadosFichaPonto( client: ConvexClient, funcionarioId: Id<'funcionarios'>, dataInicio: string, dataFim: string ): Promise<{ dias: DiaFichaPonto[]; resumo: ResumoPeriodo; config: { horarioEntrada: string; horarioSaidaAlmoco: string; horarioRetornoAlmoco: string; horarioSaida: string; }; }> { // Buscar todos os dados necessários const [ registrosFuncionario, atestadosLicencas, ausenciasTodas, ajustes, inconsistencias, homologacoes, dispensas, config ] = await Promise.all([ client.query(api.pontos.listarRegistrosPeriodo, { funcionarioId, dataInicio, dataFim }), client.query(api.atestadosLicencas.listarPorFuncionario, { funcionarioId }), client.query(api.ausencias.listarTodas, {}), client.query(api.pontos.listarAjustesBancoHoras, { funcionarioId }), client.query(api.pontos.listarInconsistenciasBancoHoras, {}), client.query(api.pontos.listarHomologacoes, { funcionarioId }), client.query(api.pontos.listarDispensas, { funcionarioId, apenasAtivas: false }), client.query(api.configuracaoPonto.obterConfiguracao, {}) ]); const atestados = atestadosLicencas?.atestados || []; const licencas = atestadosLicencas?.licencas || []; const ausencias = (ausenciasTodas || []).filter((a) => a.funcionarioId === funcionarioId); if (!config) { throw new Error('Configuração de ponto não encontrada'); } // Filtrar dados pelo período const dataInicioObj = new Date(dataInicio + 'T00:00:00'); const dataFimObj = new Date(dataFim + 'T23:59:59'); const atestadosPeriodo = (atestados || []).filter((a) => { const inicio = new Date(a.dataInicio); const fim = new Date(a.dataFim); return inicio <= dataFimObj && fim >= dataInicioObj; }); const ausenciasPeriodo = (ausencias || []).filter((a) => { const inicio = new Date(a.dataInicio); const fim = new Date(a.dataFim); return inicio <= dataFimObj && fim >= dataInicioObj; }); const licencasPeriodo = (licencas || []).filter((l) => { const inicio = new Date(l.dataInicio); const fim = new Date(l.dataFim); return inicio <= dataFimObj && fim >= dataInicioObj; }); const ajustesPeriodo = (ajustes || []).filter((a) => { const dataAjuste = new Date(a.dataAplicacao); return dataAjuste >= dataInicioObj && dataAjuste <= dataFimObj; }); const inconsistenciasPeriodo = (inconsistencias || []).filter((i) => { if (i.funcionarioId !== funcionarioId) return false; const dataInconsistencia = new Date(i.dataDetectada); return dataInconsistencia >= dataInicioObj && dataInconsistencia <= dataFimObj; }); const dataInicioTimestamp = dataInicioObj.getTime(); const dataFimTimestamp = dataFimObj.getTime(); const homologacoesPeriodo = (homologacoes || []).filter((h) => { return h.criadoEm >= dataInicioTimestamp && h.criadoEm <= dataFimTimestamp; }); const dispensasPeriodo = (dispensas || []).filter((d) => { const dispensaInicio = new Date(d.dataInicio + 'T00:00:00'); const dispensaFim = new Date(d.dataFim + 'T23:59:59'); return dispensaInicio <= dataFimObj && dispensaFim >= dataInicioObj; }); // Gerar todos os dias do período const diasPeriodo = gerarDiasPeriodo(dataInicio, dataFim); const diasProcessados: DiaFichaPonto[] = []; // Agrupar registros por data const registrosPorData: Record = {}; for (const r of registrosFuncionario || []) { if (!registrosPorData[r.data]) { registrosPorData[r.data] = []; } registrosPorData[r.data]!.push(r); } // Processar cada dia for (const data of diasPeriodo) { const dataObj = new Date(data); const regsReais = registrosPorData[data] || []; const regsEsperados = gerarRegistrosEsperados(data, config); // Verificar atestado const atestadoDia = atestadosPeriodo.find((a) => { const inicio = new Date(a.dataInicio); const fim = new Date(a.dataFim); return dataObj >= inicio && dataObj <= fim; }) || null; // Verificar ausência const ausenciaDia = ausenciasPeriodo.find((a) => { const inicio = new Date(a.dataInicio); const fim = new Date(a.dataFim); return dataObj >= inicio && dataObj <= fim; }) || null; // Verificar licença const licencaDia = licencasPeriodo.find((l) => { const inicio = new Date(l.dataInicio); const fim = new Date(l.dataFim); return dataObj >= inicio && dataObj <= fim; }) || null; // Verificar ajustes do dia const ajustesDia = ajustesPeriodo.filter((a) => a.dataAplicacao === data); // Verificar inconsistências do dia const inconsistenciasDia = inconsistenciasPeriodo.filter((i) => i.dataDetectada === data); // Verificar homologações do dia const homologacoesDia = homologacoesPeriodo.filter((h) => { if (h.registroId) { const registro = regsReais.find((r) => r._id === h.registroId); return registro !== undefined; } return false; }); // Verificar dispensa const dispensaDia = dispensasPeriodo.find((d) => { const dispensaInicio = new Date(d.dataInicio + 'T00:00:00'); const dispensaFim = new Date(d.dataFim + 'T23:59:59'); return dataObj >= dispensaInicio && dataObj <= dispensaFim; }) || null; // Calcular saldo diário const regsReaisOrdenados = [...regsReais].sort((a, b) => { if (a.hora !== b.hora) return a.hora - b.hora; return a.minuto - b.minuto; }); const saldosComparativosPorPar = calcularSaldoComparativoPorPar(regsReaisOrdenados, config); let saldoDiario: { diferencaMinutos: number; trabalhadoMinutos: number; esperadoMinutos: number } | null = null; let saldoDiarioTotalDiferencaMinutos = 0; let saldoDiarioTotalTrabalhadoMinutos = 0; let saldoDiarioTotalEsperadoMinutos = 0; // Somar saldos dos pares const paresProcessados = new Set(); for (const [, saldo] of saldosComparativosPorPar.entries()) { if (!paresProcessados.has(saldo.parIndex)) { saldoDiarioTotalDiferencaMinutos += saldo.diferencaMinutos; saldoDiarioTotalTrabalhadoMinutos += saldo.trabalhadoMinutos; saldoDiarioTotalEsperadoMinutos += saldo.esperadoMinutos; paresProcessados.add(saldo.parIndex); } } // Calcular saldo para pares não marcados const todosRegistros: Array<{ tipo: string; hora: number; minuto: number; real: boolean }> = []; for (const reg of regsReais) { todosRegistros.push({ tipo: reg.tipo, hora: reg.hora, minuto: reg.minuto, real: true }); } for (const regEsperado of regsEsperados) { if (!registroFoiMarcado(regEsperado, regsReais)) { todosRegistros.push({ tipo: regEsperado.tipo, hora: regEsperado.hora, minuto: regEsperado.minuto, real: false }); } } // Identificar pares não marcados e calcular saldo negativo for (let i = 0; i < todosRegistros.length; i++) { const reg = todosRegistros[i]; if ((reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco') && !reg.real) { const tipoSaidaEsperado = reg.tipo === 'entrada' ? 'saida_almoco' : 'saida'; const saidaEsperada = todosRegistros.find((r, idx) => { if (idx <= i) return false; if (r.tipo !== tipoSaidaEsperado || r.real) return false; const minutosEntrada = reg.hora * 60 + reg.minuto; const minutosSaidaEsperada = r.hora * 60 + r.minuto; const temRegistroRealNoIntervalo = regsReais.some((real) => { if (real.tipo !== tipoSaidaEsperado) return false; const minutosReal = real.hora * 60 + real.minuto; return minutosReal >= minutosEntrada && minutosReal < minutosSaidaEsperada; }); return !temRegistroRealNoIntervalo; }); if (saidaEsperada) { 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; } saldoDiarioTotalDiferencaMinutos -= esperadoMinutos; saldoDiarioTotalEsperadoMinutos += esperadoMinutos; } } } // Aplicar ajustes manuais for (const ajuste of ajustesDia) { if (ajuste.tipo === 'abonar') { saldoDiarioTotalDiferencaMinutos += ajuste.valorMinutos; } else if (ajuste.tipo === 'descontar') { saldoDiarioTotalDiferencaMinutos -= ajuste.valorMinutos; } } // Calcular diferença final const diferencaDiariaCorrigida = saldoDiarioTotalTrabalhadoMinutos - saldoDiarioTotalEsperadoMinutos; saldoDiario = { diferencaMinutos: diferencaDiariaCorrigida, trabalhadoMinutos: saldoDiarioTotalTrabalhadoMinutos, esperadoMinutos: saldoDiarioTotalEsperadoMinutos }; // Determinar tipo de dia let tipoDia: TipoDia = 'normal'; let computado = true; if (dispensaDia) { tipoDia = 'nao_computado'; computado = false; } else if (licencaDia) { tipoDia = 'licenca'; computado = false; } else if (atestadoDia) { tipoDia = 'atestado'; computado = false; } else if (ausenciaDia) { tipoDia = 'ausencia'; computado = false; } else if (ajustesDia.some((a) => a.tipo === 'abonar' && a.valorMinutos >= 240)) { tipoDia = 'abonado'; } if (inconsistenciasDia.length > 0) { tipoDia = 'inconsistente'; } diasProcessados.push({ data, dataFormatada: formatarDataDDMMAAAA(data), tipoDia, registros: regsReais, registrosEsperados: regsEsperados, saldoDiario, saldoAcumulado: 0, // Será calculado depois atestado: atestadoDia ? { _id: atestadoDia._id, tipo: atestadoDia.tipo, dataInicio: atestadoDia.dataInicio, dataFim: atestadoDia.dataFim, motivo: atestadoDia.observacoes } : null, ausencia: ausenciaDia ? { _id: ausenciaDia._id, motivo: ausenciaDia.motivo, dataInicio: ausenciaDia.dataInicio, dataFim: ausenciaDia.dataFim, status: ausenciaDia.status } : null, licenca: licencaDia ? { _id: licencaDia._id, tipo: licencaDia.tipo || 'licenca', dataInicio: licencaDia.dataInicio, dataFim: licencaDia.dataFim } : null, ajustes: ajustesDia.map((a) => ({ _id: a._id, tipo: a.tipo, valorMinutos: a.valorMinutos, motivoDescricao: a.motivoDescricao, gestorId: a.gestorId, dataInicio: a.dataInicio, horaInicio: a.horaInicio, minutoInicio: a.minutoInicio, dataFim: a.dataFim, horaFim: a.horaFim, minutoFim: a.minutoFim })), inconsistencias: inconsistenciasDia.map((i) => ({ _id: i._id, tipo: i.tipo, descricao: i.descricao, dataDetectada: i.dataDetectada, status: i.status, resolvidoPor: i.resolvidoPor, resolvidoEm: i.resolvidoEm })), homologacoes: homologacoesDia.map((h) => ({ _id: h._id, motivoDescricao: h.motivoDescricao, gestorId: h.gestorId })), dispensa: dispensaDia ? { _id: dispensaDia._id, motivo: dispensaDia.motivo, dataInicio: dispensaDia.dataInicio, dataFim: dispensaDia.dataFim, ativo: dispensaDia.ativo } : null, computado }); } // Calcular saldo acumulado para cada dia // Agora consideramos todos os dias que possuem saldo diário, inclusive // atestados, ausências e dias não computados, para que o resumo do período // reflita qualquer trabalho realizado e a carga horária esperada. let saldoAcumulado = 0; for (const dia of diasProcessados) { if (dia.saldoDiario) { saldoAcumulado += dia.saldoDiario.diferencaMinutos; } dia.saldoAcumulado = saldoAcumulado; } // Calcular resumo com formatações // Total de horas trabalhadas e esperadas passa a considerar todos os dias, // não apenas os marcados como "computados", para que trechos trabalhados // em dias de ausência/dispensa também apareçam no resumo. const totalHorasTrabalhadas = diasProcessados.reduce( (acc, d) => acc + (d.saldoDiario?.trabalhadoMinutos || 0), 0 ); const totalHorasEsperadas = diasProcessados.reduce( (acc, d) => acc + (d.saldoDiario?.esperadoMinutos || 0), 0 ); const diferencaTotal = diasProcessados.reduce( (acc, d) => acc + (d.saldoDiario?.diferencaMinutos || 0), 0 ); const saldoPeriodo = diferencaTotal; const saldoFinal = diasProcessados.length > 0 ? diasProcessados[diasProcessados.length - 1]!.saldoAcumulado : 0; const resumo: ResumoPeriodo = { totalDias: diasProcessados.length, diasTrabalhados: diasProcessados.filter((d) => d.computado && d.registros.length > 0).length, diasComAtestado: diasProcessados.filter((d) => d.atestado !== null).length, diasAusentes: diasProcessados.filter((d) => d.ausencia !== null).length, diasComLicenca: diasProcessados.filter((d) => d.licenca !== null).length, diasAbonados: diasProcessados.filter((d) => d.tipoDia === 'abonado').length, diasNaoComputados: diasProcessados.filter((d) => !d.computado).length, diasComInconsistencia: diasProcessados.filter((d) => d.inconsistencias.length > 0).length, totalHorasTrabalhadas, totalHorasEsperadas, diferencaTotal, saldoInicial: 0, saldoFinal, saldoPeriodo, totalInconsistencias: inconsistenciasPeriodo.length, saldoInicialFormatado: formatarMinutos(0), saldoPeriodoFormatado: formatarMinutos(saldoPeriodo), saldoFinalFormatado: formatarMinutos(saldoFinal), totalHorasTrabalhadasFormatado: formatarHoras(totalHorasTrabalhadas), totalHorasEsperadasFormatado: formatarHoras(totalHorasEsperadas), diferencaTotalFormatado: formatarMinutos(diferencaTotal) }; return { dias: diasProcessados, resumo, config }; }