import { v } from 'convex/values'; import { query } from './_generated/server'; import { Id } from './_generated/dataModel'; import type { QueryCtx } from './_generated/server'; /** * SISTEMA DE CÁLCULO DE SALDO DE FÉRIAS * Suporte a múltiplos regimes de trabalho * * ============================================ * REGRAS CLT (Consolidação das Leis do Trabalho): * ============================================ * - 30 dias de férias por ano trabalhado * - Período aquisitivo: 12 meses de trabalho * - Período concessivo: 12 meses após aquisitivo * - Pode dividir em até 3 períodos * - Um período deve ter no mínimo 14 dias * - Demais períodos: mínimo 5 dias cada * - Abono pecuniário: vender 1/3 das férias (10 dias) - OPCIONAL * * ============================================ * REGRAS SERVIDOR PÚBLICO ESTADUAL DE PERNAMBUCO * Lei nº 6.123/1968 - Estatuto dos Funcionários Públicos Civis do Estado de PE * ============================================ * - 30 dias de férias por ano de exercício * - Pode dividir em até 2 períodos (NÃO 3) * - Nenhum período pode ser inferior a 10 dias (NÃO 5) * - NÃO permite abono pecuniário (venda de férias) * - Férias devem ser gozadas no ano subsequente * - Servidor com mais de 10 anos: pode acumular até 2 períodos * - Preferência: férias no período de 20/12 a 10/01 para docentes * - Gestante: pode antecipar ou prorrogar férias * * ============================================ * REGRAS SERVIDOR PÚBLICO MUNICIPAL * ============================================ * - Seguem as mesmas diretrizes do regime estadual acima */ type RegimeTrabalho = 'clt' | 'estatutario_pe' | 'estatutario_federal' | 'estatutario_municipal'; // Configurações por regime const REGIMES_CONFIG = { clt: { nome: 'CLT - Consolidação das Leis do Trabalho', maxPeriodos: 3, minDiasPeriodo: 5, minDiasPeriodoPrincipal: 14, abonoPermitido: true, maxDiasAbono: 10 }, estatutario_pe: { nome: 'Servidor Público Estadual de Pernambuco', maxPeriodos: 2, minDiasPeriodo: 15, // Mínimo 15 dias por período minDiasPeriodoPrincipal: null, // Não há essa regra abonoPermitido: false, maxDiasAbono: 0, periodosPermitidos: [15, 30] // Apenas 15 ou 30 dias por período }, estatutario_federal: { nome: 'Servidor Público Federal', maxPeriodos: 3, minDiasPeriodo: 5, minDiasPeriodoPrincipal: 14, abonoPermitido: true, maxDiasAbono: 10 }, estatutario_municipal: { nome: 'Servidor Público Municipal', maxPeriodos: 2, minDiasPeriodo: 15, // Mínimo 15 dias por período minDiasPeriodoPrincipal: null, abonoPermitido: false, maxDiasAbono: 0, periodosPermitidos: [15, 30] // Apenas 15 ou 30 dias por período } }; // Helper: Calcular dias entre duas datas function calcularDiasEntreDatas(dataInicio: string, dataFim: string): number { const inicio = new Date(dataInicio); const fim = new Date(dataFim); const diffTime = Math.abs(fim.getTime() - inicio.getTime()); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; // +1 para incluir ambos os dias return diffDays; } // Helper: Calcular data de fim do período aquisitivo function calcularDataFimPeriodo(dataAdmissao: string, anosPassados: number): string { const dataInicio = new Date(dataAdmissao); dataInicio.setFullYear(dataInicio.getFullYear() + anosPassados); return dataInicio.toISOString().split('T')[0]; } // Helper: Obter regime de trabalho do funcionário async function obterRegimeTrabalho( ctx: QueryCtx, funcionarioId: Id<'funcionarios'> ): Promise { const funcionario = await ctx.db.get(funcionarioId); return funcionario?.regimeTrabalho || 'clt'; // Default CLT } // Helper: Calcular saldo dinamicamente baseado na tabela ferias async function calcularSaldo( ctx: QueryCtx, funcionarioId: Id<'funcionarios'>, anoReferencia: number, feriasIdExcluir?: Id<'ferias'> // ID do período a excluir do cálculo (para ajustes) ): Promise<{ diasDireito: number; diasUsados: number; diasPendentes: number; diasDisponiveis: number; diasAbono: number; dataInicio: string; dataFim: string; status: 'ativo' | 'vencido' | 'concluido'; } | null> { const funcionario = await ctx.db.get(funcionarioId); if (!funcionario || !funcionario.admissaoData) return null; const regime = funcionario.regimeTrabalho || 'clt'; const config = REGIMES_CONFIG[regime]; // Calcular anos desde admissão const dataAdmissao = new Date(funcionario.admissaoData); const anosDesdeAdmissao = anoReferencia - dataAdmissao.getFullYear(); if (anosDesdeAdmissao < 1) return null; // Ainda não tem direito const dataInicio = calcularDataFimPeriodo(funcionario.admissaoData, anosDesdeAdmissao - 1); const dataFim = calcularDataFimPeriodo(funcionario.admissaoData, anosDesdeAdmissao); // Buscar todos os registros de férias para este funcionário e ano const todasFerias = await ctx.db .query('ferias') .withIndex('by_funcionario_and_ano', (q) => q.eq('funcionarioId', funcionarioId).eq('anoReferencia', anoReferencia) ) .collect(); // Filtrar períodos a excluir (para ajustes) const feriasFiltradas = feriasIdExcluir ? todasFerias.filter((f) => f._id !== feriasIdExcluir) : todasFerias; // Calcular dias usados (aprovado, data_ajustada_aprovada, EmFérias) const diasUsados = feriasFiltradas .filter( (f) => f.status === 'aprovado' || f.status === 'data_ajustada_aprovada' || f.status === 'EmFérias' ) .reduce((acc, f) => acc + f.diasFerias, 0); // Calcular dias pendentes (aguardando_aprovacao) const diasPendentes = feriasFiltradas .filter((f) => f.status === 'aguardando_aprovacao') .reduce((acc, f) => acc + f.diasFerias, 0); // Calcular dias de abono const diasAbono = feriasFiltradas.reduce((acc, f) => acc + f.diasAbono, 0); // Calcular dias disponíveis const diasDireito = 30; const diasDisponiveis = diasDireito - diasUsados - diasPendentes - diasAbono; // Determinar status do período const hoje = new Date(); const dataFimPeriodo = new Date(dataFim); let status: 'ativo' | 'vencido' | 'concluido'; if (diasDireito - diasUsados - diasAbono <= 0) { status = 'concluido'; } else if (hoje > dataFimPeriodo) { status = 'vencido'; } else { status = 'ativo'; } return { diasDireito, diasUsados, diasPendentes, diasDisponiveis, diasAbono, dataInicio, dataFim, status }; } /** * Query: Obter saldo de férias de um funcionário para um ano específico */ export const obterSaldo = query({ args: { funcionarioId: v.id('funcionarios'), anoReferencia: v.number() }, returns: v.union( v.object({ anoReferencia: v.number(), diasDireito: v.number(), diasUsados: v.number(), diasPendentes: v.number(), diasDisponiveis: v.number(), diasAbono: v.number(), abonoPermitido: v.boolean(), status: v.union(v.literal('ativo'), v.literal('vencido'), v.literal('concluido')), dataInicio: v.string(), dataFim: v.string(), regimeTrabalho: v.string() }), v.null() ), handler: async (ctx, args) => { const saldo = await calcularSaldo(ctx, args.funcionarioId, args.anoReferencia); if (!saldo) return null; const funcionario = await ctx.db.get(args.funcionarioId); const regime = funcionario?.regimeTrabalho || 'clt'; const config = REGIMES_CONFIG[regime]; return { anoReferencia: args.anoReferencia, ...saldo, abonoPermitido: config.abonoPermitido, regimeTrabalho: config.nome }; } }); /** * Query: Listar todos os saldos de um funcionário */ export const listarSaldos = query({ args: { funcionarioId: v.id('funcionarios') }, returns: v.array( v.object({ anoReferencia: v.number(), diasDireito: v.number(), diasUsados: v.number(), diasPendentes: v.number(), diasDisponiveis: v.number(), diasAbono: v.number(), abonoPermitido: v.boolean(), status: v.union(v.literal('ativo'), v.literal('vencido'), v.literal('concluido')), dataInicio: v.string(), dataFim: v.string() }) ), handler: async (ctx, args) => { const funcionario = await ctx.db.get(args.funcionarioId); if (!funcionario || !funcionario.admissaoData) return []; const regime = funcionario.regimeTrabalho || 'clt'; const config = REGIMES_CONFIG[regime]; const dataAdmissao = new Date(funcionario.admissaoData); const anoAtual = new Date().getFullYear(); const anosDesdeAdmissao = anoAtual - dataAdmissao.getFullYear(); const saldos = []; // Calcular saldos para os últimos 3 anos (atual, anterior e anterior ao anterior) for (let i = 0; i < 3; i++) { const ano = anoAtual - i; const anosPeriodo = ano - dataAdmissao.getFullYear(); if (anosPeriodo < 1) continue; const saldo = await calcularSaldo(ctx, args.funcionarioId, ano); if (saldo) { saldos.push({ anoReferencia: ano, ...saldo, abonoPermitido: config.abonoPermitido }); } } return saldos; } }); /** * Query: Validar solicitação de férias (regras CLT ou Servidor Público PE) */ export const validarSolicitacao = query({ args: { funcionarioId: v.id('funcionarios'), anoReferencia: v.number(), periodos: v.array( v.object({ dataInicio: v.string(), dataFim: v.string() }) ), feriasIdExcluir: v.optional(v.id('ferias')) // ID do período a excluir do cálculo de saldo (para ajustes) }, returns: v.object({ valido: v.boolean(), erros: v.array(v.string()), avisos: v.array(v.string()), totalDias: v.number(), regimeTrabalho: v.string() }), handler: async (ctx, args) => { const erros: string[] = []; const avisos: string[] = []; let totalDias = 0; // Obter regime de trabalho const regime = await obterRegimeTrabalho(ctx, args.funcionarioId); const config = REGIMES_CONFIG[regime]; // Validação 1: Número de períodos if (args.periodos.length === 0) { erros.push('É necessário adicionar pelo menos 1 período de férias'); } if (args.periodos.length > config.maxPeriodos) { erros.push(`Máximo de ${config.maxPeriodos} períodos permitidos para ${config.nome}`); } // Calcular dias de cada período e validar const diasPorPeriodo: number[] = []; for (const periodo of args.periodos) { const dias = calcularDiasEntreDatas(periodo.dataInicio, periodo.dataFim); diasPorPeriodo.push(dias); totalDias += dias; // Validação 2: Mínimo de dias por período if (dias < config.minDiasPeriodo) { erros.push( `Período de ${dias} dias é inválido. Mínimo: ${config.minDiasPeriodo} dias corridos (${config.nome})` ); } // Validação específica para regime estatutário PE e Municipal if ( (regime === 'estatutario_pe' || regime === 'estatutario_municipal') && 'periodosPermitidos' in config ) { if (!config.periodosPermitidos.includes(dias)) { erros.push( `Para ${config.nome}, os períodos devem ter exatamente 15 ou 30 dias. Período de ${dias} dias não é permitido.` ); } } } // Validação específica para regime estatutário PE e Municipal // Permite períodos fracionados: cada período deve ser 15 ou 30 dias // Total não pode exceder 30 dias, mas pode ser menos (períodos fracionados) if (regime === 'estatutario_pe' || regime === 'estatutario_municipal') { // Verificar se cada período individual é válido (15 ou 30 dias) for (const dias of diasPorPeriodo) { if (dias !== 15 && dias !== 30) { erros.push( `Para ${config.nome}, cada período deve ter exatamente 15 ou 30 dias. Período de ${dias} dias não é permitido.` ); } } // Total não pode exceder 30 dias if (totalDias > 30) { erros.push( `Para ${config.nome}, o total de dias não pode exceder 30 dias. Total solicitado: ${totalDias} dias.` ); } // Máximo de 2 períodos if (args.periodos.length > 2) { erros.push(`Para ${config.nome}, o máximo de períodos permitidos é 2.`); } } // Validação 3: CLT requer um período com 14+ dias se dividir if (regime === 'clt' && args.periodos.length > 1 && config.minDiasPeriodoPrincipal) { const temPeriodo14Dias = diasPorPeriodo.some((d) => d >= config.minDiasPeriodoPrincipal!); if (!temPeriodo14Dias) { erros.push( `Ao dividir férias em CLT, um período deve ter no mínimo ${config.minDiasPeriodoPrincipal} dias corridos` ); } } // Validação 4: Verificar saldo disponível (calculado dinamicamente) // Se for um ajuste (feriasIdExcluir fornecido), excluir esse período do cálculo const saldo = await calcularSaldo( ctx, args.funcionarioId, args.anoReferencia, args.feriasIdExcluir ); if (!saldo) { erros.push(`Você ainda não tem direito a férias referentes ao ano ${args.anoReferencia}`); } else { // Verificar saldo disponível (já excluindo o período original se for ajuste) if (totalDias > saldo.diasDisponiveis) { erros.push( `Total solicitado (${totalDias} dias) excede saldo disponível (${saldo.diasDisponiveis} dias)` ); } // Aviso: Saldo baixo if (saldo.diasDisponiveis < 15 && saldo.diasDisponiveis > totalDias) { avisos.push( `Após essa solicitação, restará ${saldo.diasDisponiveis - totalDias} dias de ${args.anoReferencia}` ); } // Aviso: Férias vencendo const hoje = new Date(); const dataFim = new Date(saldo.dataFim); const diasAteVencer = Math.ceil((dataFim.getTime() - hoje.getTime()) / (1000 * 60 * 60 * 24)); if (diasAteVencer < 90 && diasAteVencer > 0) { avisos.push( `⚠️ Atenção: Seu período aquisitivo ${args.anoReferencia} vence em ${diasAteVencer} dias!` ); } if (diasAteVencer < 0) { avisos.push( `⚠️ URGENTE: Seu período aquisitivo ${args.anoReferencia} está VENCIDO há ${Math.abs(diasAteVencer)} dias!` ); } } // Validação 5: Verificar conflitos de datas (sobreposição) for (let i = 0; i < args.periodos.length; i++) { for (let j = i + 1; j < args.periodos.length; j++) { const inicio1 = new Date(args.periodos[i].dataInicio); const fim1 = new Date(args.periodos[i].dataFim); const inicio2 = new Date(args.periodos[j].dataInicio); const fim2 = new Date(args.periodos[j].dataFim); if ((inicio1 <= fim2 && fim1 >= inicio2) || (inicio2 <= fim1 && fim2 >= inicio1)) { erros.push('Os períodos não podem se sobrepor'); } } } // Validação 6: Datas no futuro (aviso) const hoje = new Date(); hoje.setHours(0, 0, 0, 0); for (const periodo of args.periodos) { const inicio = new Date(periodo.dataInicio); if (inicio < hoje) { avisos.push('⚠️ Período(s) com data de início no passado ou hoje'); break; } } // Validação 7: Servidor PE - aviso sobre período preferencial para docentes if (regime === 'estatutario_pe' || regime === 'estatutario_municipal') { for (const periodo of args.periodos) { const mes = new Date(periodo.dataInicio).getMonth() + 1; if (mes === 12 || mes === 1) { avisos.push('📅 Período preferencial para docentes (20/12 a 10/01)'); break; } } } return { valido: erros.length === 0, erros, avisos, totalDias, regimeTrabalho: config.nome }; } });