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, }; }, });