- Added regime de trabalho selection to employee forms for better categorization. - Updated backend validation to include regime de trabalho options for employees. - Enhanced employee data handling by integrating regime de trabalho into various components. - Removed the print modal for financial data to streamline the employee profile interface. - Improved overall code clarity and maintainability across multiple files.
554 lines
17 KiB
TypeScript
554 lines
17 KiB
TypeScript
import { v } from "convex/values";
|
|
import { query, mutation, internalMutation } from "./_generated/server";
|
|
import { internal } from "./_generated/api";
|
|
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: 10,
|
|
minDiasPeriodoPrincipal: null, // Não há essa regra
|
|
abonoPermitido: false,
|
|
maxDiasAbono: 0,
|
|
},
|
|
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: 10,
|
|
minDiasPeriodoPrincipal: null,
|
|
abonoPermitido: false,
|
|
maxDiasAbono: 0,
|
|
},
|
|
};
|
|
|
|
// 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<RegimeTrabalho> {
|
|
const funcionario = await ctx.db.get(funcionarioId);
|
|
return funcionario?.regimeTrabalho || "clt"; // Default CLT
|
|
}
|
|
|
|
/**
|
|
* 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) => {
|
|
// Buscar período aquisitivo
|
|
const periodo = await ctx.db
|
|
.query("periodosAquisitivos")
|
|
.withIndex("by_funcionario_and_ano", (q) =>
|
|
q.eq("funcionarioId", args.funcionarioId).eq("anoReferencia", args.anoReferencia)
|
|
)
|
|
.first();
|
|
|
|
if (!periodo) {
|
|
// Se não existe, calcular e retornar dados previstos sem mutar o banco
|
|
const funcionario = await ctx.db.get(args.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 = args.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
|
|
);
|
|
|
|
return {
|
|
anoReferencia: args.anoReferencia,
|
|
diasDireito: 30,
|
|
diasUsados: 0,
|
|
diasPendentes: 0,
|
|
diasDisponiveis: 30,
|
|
diasAbono: 0,
|
|
abonoPermitido: config.abonoPermitido,
|
|
status: "ativo" as const,
|
|
dataInicio,
|
|
dataFim,
|
|
regimeTrabalho: config.nome,
|
|
};
|
|
}
|
|
|
|
const funcionario = await ctx.db.get(args.funcionarioId);
|
|
const regime = funcionario?.regimeTrabalho || "clt";
|
|
const config = REGIMES_CONFIG[regime];
|
|
|
|
return {
|
|
anoReferencia: periodo.anoReferencia,
|
|
diasDireito: periodo.diasDireito,
|
|
diasUsados: periodo.diasUsados,
|
|
diasPendentes: periodo.diasPendentes,
|
|
diasDisponiveis: periodo.diasDisponiveis,
|
|
diasAbono: periodo.diasAbono,
|
|
abonoPermitido: config.abonoPermitido,
|
|
status: periodo.status,
|
|
dataInicio: periodo.dataInicio,
|
|
dataFim: periodo.dataFim,
|
|
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({
|
|
_id: v.id("periodosAquisitivos"),
|
|
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 periodos = await ctx.db
|
|
.query("periodosAquisitivos")
|
|
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId))
|
|
.collect();
|
|
|
|
return periodos.map((p) => ({
|
|
_id: p._id,
|
|
anoReferencia: p.anoReferencia,
|
|
diasDireito: p.diasDireito,
|
|
diasUsados: p.diasUsados,
|
|
diasPendentes: p.diasPendentes,
|
|
diasDisponiveis: p.diasDisponiveis,
|
|
diasAbono: p.diasAbono,
|
|
abonoPermitido: p.abonoPermitido,
|
|
status: p.status,
|
|
dataInicio: p.dataInicio,
|
|
dataFim: p.dataFim,
|
|
}));
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 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(),
|
|
})
|
|
),
|
|
},
|
|
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 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
|
|
const periodo = await ctx.db
|
|
.query("periodosAquisitivos")
|
|
.withIndex("by_funcionario_and_ano", (q) =>
|
|
q.eq("funcionarioId", args.funcionarioId).eq("anoReferencia", args.anoReferencia)
|
|
)
|
|
.first();
|
|
|
|
if (!periodo) {
|
|
erros.push(`Você ainda não tem direito a férias referentes ao ano ${args.anoReferencia}`);
|
|
} else {
|
|
if (totalDias > periodo.diasDisponiveis) {
|
|
erros.push(
|
|
`Total solicitado (${totalDias} dias) excede saldo disponível (${periodo.diasDisponiveis} dias)`
|
|
);
|
|
}
|
|
|
|
// Aviso: Saldo baixo
|
|
if (periodo.diasDisponiveis < 15 && periodo.diasDisponiveis > totalDias) {
|
|
avisos.push(
|
|
`Após essa solicitação, restará ${periodo.diasDisponiveis - totalDias} dias de ${args.anoReferencia}`
|
|
);
|
|
}
|
|
|
|
// Aviso: Férias vencendo
|
|
const hoje = new Date();
|
|
const dataFim = new Date(periodo.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 ${periodo.anoReferencia} vence em ${diasAteVencer} dias!`
|
|
);
|
|
}
|
|
|
|
if (diasAteVencer < 0) {
|
|
avisos.push(
|
|
`⚠️ URGENTE: Seu período aquisitivo ${periodo.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,
|
|
};
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Internal Mutation: Atualizar saldo após aprovação de férias
|
|
*/
|
|
export const atualizarSaldoAposAprovacao = internalMutation({
|
|
args: {
|
|
solicitacaoId: v.id("solicitacoesFerias"),
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const solicitacao = await ctx.db.get(args.solicitacaoId);
|
|
if (!solicitacao) return null;
|
|
|
|
// Buscar período aquisitivo
|
|
const periodo = await ctx.db
|
|
.query("periodosAquisitivos")
|
|
.withIndex("by_funcionario_and_ano", (q) =>
|
|
q.eq("funcionarioId", solicitacao.funcionarioId).eq("anoReferencia", solicitacao.anoReferencia)
|
|
)
|
|
.first();
|
|
|
|
if (!periodo) return null;
|
|
|
|
// Calcular total de dias
|
|
let totalDias = 0;
|
|
for (const p of solicitacao.periodos) {
|
|
totalDias += p.diasCorridos;
|
|
}
|
|
|
|
// Atualizar saldo
|
|
await ctx.db.patch(periodo._id, {
|
|
diasPendentes: periodo.diasPendentes - totalDias,
|
|
diasUsados: periodo.diasUsados + totalDias,
|
|
diasDisponiveis: periodo.diasDireito - (periodo.diasUsados + totalDias) - periodo.diasAbono,
|
|
status: periodo.diasDireito - (periodo.diasUsados + totalDias) <= 0 ? "concluido" : periodo.status,
|
|
});
|
|
|
|
return null;
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Internal Mutation: Reservar dias (ao criar solicitação)
|
|
*/
|
|
export const reservarDias = internalMutation({
|
|
args: {
|
|
funcionarioId: v.id("funcionarios"),
|
|
anoReferencia: v.number(),
|
|
totalDias: v.number(),
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const periodo = await ctx.db
|
|
.query("periodosAquisitivos")
|
|
.withIndex("by_funcionario_and_ano", (q) =>
|
|
q.eq("funcionarioId", args.funcionarioId).eq("anoReferencia", args.anoReferencia)
|
|
)
|
|
.first();
|
|
|
|
if (!periodo) return null;
|
|
|
|
await ctx.db.patch(periodo._id, {
|
|
diasPendentes: periodo.diasPendentes + args.totalDias,
|
|
diasDisponiveis: periodo.diasDisponiveis - args.totalDias,
|
|
});
|
|
|
|
return null;
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Internal Mutation: Liberar dias (ao reprovar solicitação)
|
|
*/
|
|
export const liberarDias = internalMutation({
|
|
args: {
|
|
solicitacaoId: v.id("solicitacoesFerias"),
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const solicitacao = await ctx.db.get(args.solicitacaoId);
|
|
if (!solicitacao) return null;
|
|
|
|
const periodo = await ctx.db
|
|
.query("periodosAquisitivos")
|
|
.withIndex("by_funcionario_and_ano", (q) =>
|
|
q.eq("funcionarioId", solicitacao.funcionarioId).eq("anoReferencia", solicitacao.anoReferencia)
|
|
)
|
|
.first();
|
|
|
|
if (!periodo) return null;
|
|
|
|
let totalDias = 0;
|
|
for (const p of solicitacao.periodos) {
|
|
totalDias += p.diasCorridos;
|
|
}
|
|
|
|
await ctx.db.patch(periodo._id, {
|
|
diasPendentes: periodo.diasPendentes - totalDias,
|
|
diasDisponiveis: periodo.diasDisponiveis + totalDias,
|
|
});
|
|
|
|
return null;
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Internal Mutation: Criar períodos aquisitivos para todos os funcionários
|
|
*/
|
|
export const criarPeriodosAquisitivos = internalMutation({
|
|
args: {},
|
|
returns: v.null(),
|
|
handler: async (ctx) => {
|
|
const funcionarios = await ctx.db.query("funcionarios").collect();
|
|
const anoAtual = new Date().getFullYear();
|
|
|
|
for (const func of funcionarios) {
|
|
if (!func.admissaoData) continue;
|
|
|
|
const regime = func.regimeTrabalho || "clt";
|
|
const config = REGIMES_CONFIG[regime];
|
|
|
|
const dataAdmissao = new Date(func.admissaoData);
|
|
const anosDesdeAdmissao = anoAtual - dataAdmissao.getFullYear();
|
|
|
|
// Criar períodos para os últimos 2 anos (atual e anterior)
|
|
for (let i = 0; i < 2; i++) {
|
|
const ano = anoAtual - i;
|
|
const anosPeriodo = ano - dataAdmissao.getFullYear();
|
|
|
|
if (anosPeriodo < 1) continue;
|
|
|
|
// Verificar se já existe
|
|
const periodoExistente = await ctx.db
|
|
.query("periodosAquisitivos")
|
|
.withIndex("by_funcionario_and_ano", (q) =>
|
|
q.eq("funcionarioId", func._id).eq("anoReferencia", ano)
|
|
)
|
|
.first();
|
|
|
|
if (periodoExistente) continue;
|
|
|
|
const dataInicio = calcularDataFimPeriodo(func.admissaoData, anosPeriodo - 1);
|
|
const dataFim = calcularDataFimPeriodo(func.admissaoData, anosPeriodo);
|
|
|
|
await ctx.db.insert("periodosAquisitivos", {
|
|
funcionarioId: func._id,
|
|
anoReferencia: ano,
|
|
dataInicio,
|
|
dataFim,
|
|
diasDireito: 30,
|
|
diasUsados: 0,
|
|
diasPendentes: 0,
|
|
diasDisponiveis: 30,
|
|
abonoPermitido: config.abonoPermitido,
|
|
diasAbono: 0,
|
|
status: "ativo",
|
|
});
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
});
|