700 lines
22 KiB
TypeScript
700 lines
22 KiB
TypeScript
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<string>();
|
|
|
|
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<string, (typeof grupo.registrosPorData)[string]> = {};
|
|
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<number>();
|
|
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<string, RegistroPonto[]> = {};
|
|
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<number>();
|
|
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
|
|
};
|
|
}
|
|
|