feat: enhance vacation management system with new employee association functionality, improved email notification handling, and comprehensive documentation; update dependencies and UI components for better user experience
This commit is contained in:
6
packages/backend/convex/_generated/api.d.ts
vendored
6
packages/backend/convex/_generated/api.d.ts
vendored
@@ -17,6 +17,8 @@ import type * as betterAuth_adapter from "../betterAuth/adapter.js";
|
||||
import type * as betterAuth_auth from "../betterAuth/auth.js";
|
||||
import type * as chat from "../chat.js";
|
||||
import type * as configuracaoEmail from "../configuracaoEmail.js";
|
||||
import type * as criarFuncionarioTeste from "../criarFuncionarioTeste.js";
|
||||
import type * as criarUsuarioTeste from "../criarUsuarioTeste.js";
|
||||
import type * as crons from "../crons.js";
|
||||
import type * as cursos from "../cursos.js";
|
||||
import type * as dashboard from "../dashboard.js";
|
||||
@@ -36,6 +38,7 @@ import type * as migrarUsuariosAdmin from "../migrarUsuariosAdmin.js";
|
||||
import type * as monitoramento from "../monitoramento.js";
|
||||
import type * as perfisCustomizados from "../perfisCustomizados.js";
|
||||
import type * as roles from "../roles.js";
|
||||
import type * as saldoFerias from "../saldoFerias.js";
|
||||
import type * as seed from "../seed.js";
|
||||
import type * as simbolos from "../simbolos.js";
|
||||
import type * as solicitacoesAcesso from "../solicitacoesAcesso.js";
|
||||
@@ -69,6 +72,8 @@ declare const fullApi: ApiFromModules<{
|
||||
"betterAuth/auth": typeof betterAuth_auth;
|
||||
chat: typeof chat;
|
||||
configuracaoEmail: typeof configuracaoEmail;
|
||||
criarFuncionarioTeste: typeof criarFuncionarioTeste;
|
||||
criarUsuarioTeste: typeof criarUsuarioTeste;
|
||||
crons: typeof crons;
|
||||
cursos: typeof cursos;
|
||||
dashboard: typeof dashboard;
|
||||
@@ -88,6 +93,7 @@ declare const fullApi: ApiFromModules<{
|
||||
monitoramento: typeof monitoramento;
|
||||
perfisCustomizados: typeof perfisCustomizados;
|
||||
roles: typeof roles;
|
||||
saldoFerias: typeof saldoFerias;
|
||||
seed: typeof seed;
|
||||
simbolos: typeof simbolos;
|
||||
solicitacoesAcesso: typeof solicitacoesAcesso;
|
||||
|
||||
@@ -187,6 +187,7 @@ export const enviarMensagem = mutation({
|
||||
arquivoTamanho: v.optional(v.number()),
|
||||
arquivoTipo: v.optional(v.string()),
|
||||
mencoes: v.optional(v.array(v.id("usuarios"))),
|
||||
permitirNotificacaoParaSiMesmo: v.optional(v.boolean()), // ✅ NOVO: Permite criar notificação para si mesmo
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
@@ -219,10 +220,14 @@ export const enviarMensagem = mutation({
|
||||
ultimaMensagemTimestamp: Date.now(),
|
||||
});
|
||||
|
||||
// Criar notificações para outros participantes (com tratamento de erro)
|
||||
// Criar notificações para participantes (com tratamento de erro)
|
||||
try {
|
||||
for (const participanteId of conversa.participantes) {
|
||||
if (participanteId !== usuarioAtual._id) {
|
||||
// ✅ MODIFICADO: Permite notificação para si mesmo se flag estiver ativa
|
||||
const ehOMesmoUsuario = participanteId === usuarioAtual._id;
|
||||
const deveCriarNotificacao = !ehOMesmoUsuario || args.permitirNotificacaoParaSiMesmo;
|
||||
|
||||
if (deveCriarNotificacao) {
|
||||
const tipoNotificacao = args.mencoes?.includes(participanteId)
|
||||
? "mencao"
|
||||
: "nova_mensagem";
|
||||
@@ -593,15 +598,20 @@ export const listarConversas = query({
|
||||
if (conversa.tipo === "individual") {
|
||||
const outroUsuarioRaw = participantes.find((p) => p?._id !== usuarioAtual._id);
|
||||
if (outroUsuarioRaw) {
|
||||
// Adicionar URL da foto de perfil
|
||||
let fotoPerfilUrl = null;
|
||||
if (outroUsuarioRaw.fotoPerfil) {
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(outroUsuarioRaw.fotoPerfil);
|
||||
// 🔄 BUSCAR DADOS ATUALIZADOS DO USUÁRIO (não usar snapshot)
|
||||
const usuarioAtualizado = await ctx.db.get(outroUsuarioRaw._id);
|
||||
|
||||
if (usuarioAtualizado) {
|
||||
// Adicionar URL da foto de perfil
|
||||
let fotoPerfilUrl = null;
|
||||
if (usuarioAtualizado.fotoPerfil) {
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtualizado.fotoPerfil);
|
||||
}
|
||||
outroUsuario = {
|
||||
...usuarioAtualizado,
|
||||
fotoPerfilUrl,
|
||||
};
|
||||
}
|
||||
outroUsuario = {
|
||||
...outroUsuarioRaw,
|
||||
fotoPerfilUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
127
packages/backend/convex/criarFuncionarioTeste.ts
Normal file
127
packages/backend/convex/criarFuncionarioTeste.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation } from "./_generated/server";
|
||||
|
||||
/**
|
||||
* Mutation de teste para criar um funcionário e associar ao usuário TI Master
|
||||
* Isso permite testar o sistema de férias completo
|
||||
*/
|
||||
export const criarFuncionarioParaTIMaster = mutation({
|
||||
args: {
|
||||
usuarioEmail: v.string(), // Email do usuário TI Master
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), funcionarioId: v.id("funcionarios") }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar usuário
|
||||
const usuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", args.usuarioEmail))
|
||||
.first();
|
||||
|
||||
if (!usuario) {
|
||||
return { sucesso: false as const, erro: "Usuário não encontrado" };
|
||||
}
|
||||
|
||||
// Verificar se já tem funcionário associado
|
||||
if (usuario.funcionarioId) {
|
||||
return { sucesso: false as const, erro: "Usuário já tem funcionário associado" };
|
||||
}
|
||||
|
||||
// Buscar um símbolo qualquer (pegamos o primeiro)
|
||||
const simbolo = await ctx.db.query("simbolos").first();
|
||||
|
||||
if (!simbolo) {
|
||||
return { sucesso: false as const, erro: "Nenhum símbolo encontrado no sistema" };
|
||||
}
|
||||
|
||||
// Criar funcionário de teste
|
||||
const funcionarioId = await ctx.db.insert("funcionarios", {
|
||||
nome: usuario.nome,
|
||||
cpf: "000.000.000-00", // CPF de teste
|
||||
rg: "0000000",
|
||||
endereco: "Endereço de Teste",
|
||||
bairro: "Centro",
|
||||
cidade: "Recife",
|
||||
uf: "PE",
|
||||
telefone: "(81) 99999-9999",
|
||||
email: usuario.email,
|
||||
matricula: usuario.matricula,
|
||||
admissaoData: "2023-01-01", // Data de admissão: 1 ano atrás
|
||||
simboloId: simbolo._id,
|
||||
simboloTipo: simbolo.tipo,
|
||||
statusFerias: "ativo",
|
||||
|
||||
// IMPORTANTE: Definir regime de trabalho
|
||||
// Altere aqui para testar diferentes regimes:
|
||||
// - "clt" = CLT (máx 3 períodos, mín 5 dias)
|
||||
// - "estatutario_pe" = Servidor Público PE (máx 2 períodos, mín 10 dias)
|
||||
regimeTrabalho: "clt",
|
||||
|
||||
// Dados opcionais
|
||||
descricaoCargo: "Gestor de TI - Cargo de Teste",
|
||||
nomePai: "Pai de Teste",
|
||||
nomeMae: "Mãe de Teste",
|
||||
naturalidade: "Recife",
|
||||
naturalidadeUF: "PE",
|
||||
sexo: "masculino",
|
||||
estadoCivil: "solteiro",
|
||||
nacionalidade: "Brasileira",
|
||||
grauInstrucao: "superior_completo",
|
||||
tipoSanguineo: "O+",
|
||||
});
|
||||
|
||||
// Associar funcionário ao usuário
|
||||
await ctx.db.patch(usuario._id, {
|
||||
funcionarioId,
|
||||
});
|
||||
|
||||
return { sucesso: true as const, funcionarioId };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation para alterar o regime de trabalho de um funcionário
|
||||
* Útil para testar diferentes regras (CLT vs Servidor PE)
|
||||
*/
|
||||
export const alterarRegimeTrabalho = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
novoRegime: v.union(
|
||||
v.literal("clt"),
|
||||
v.literal("estatutario_pe"),
|
||||
v.literal("estatutario_federal"),
|
||||
v.literal("estatutario_municipal")
|
||||
),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean() }),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.funcionarioId, {
|
||||
regimeTrabalho: args.novoRegime,
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation para alterar data de admissão
|
||||
* Útil para testar diferentes períodos aquisitivos
|
||||
*/
|
||||
export const alterarDataAdmissao = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
novaData: v.string(), // Formato: "YYYY-MM-DD"
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean() }),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.funcionarioId, {
|
||||
admissaoData: args.novaData,
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
118
packages/backend/convex/criarUsuarioTeste.ts
Normal file
118
packages/backend/convex/criarUsuarioTeste.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation } from "./_generated/server";
|
||||
import { hashPassword } from "./auth/utils";
|
||||
|
||||
/**
|
||||
* Cria um usuário de teste com funcionário associado
|
||||
* para testar o sistema de férias
|
||||
*/
|
||||
export const criarUsuarioParaTesteFerias = mutation({
|
||||
args: {},
|
||||
returns: v.object({
|
||||
sucesso: v.boolean(),
|
||||
login: v.string(),
|
||||
senha: v.string(),
|
||||
mensagem: v.string(),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const loginTeste = "teste.ferias";
|
||||
const senhaTeste = "Teste@2025";
|
||||
const emailTeste = "teste.ferias@sgse.pe.gov.br";
|
||||
const nomeTeste = "João Silva (Teste)";
|
||||
|
||||
// Verificar se já existe
|
||||
const usuarioExistente = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", loginTeste))
|
||||
.first();
|
||||
|
||||
if (usuarioExistente) {
|
||||
return {
|
||||
sucesso: true,
|
||||
login: loginTeste,
|
||||
senha: senhaTeste,
|
||||
mensagem: "Usuário de teste já existe! Use as credenciais abaixo.",
|
||||
};
|
||||
}
|
||||
|
||||
// Buscar role padrão (usuário comum)
|
||||
const roleUsuario = await ctx.db
|
||||
.query("roles")
|
||||
.filter((q) => q.eq(q.field("nome"), "usuario"))
|
||||
.first();
|
||||
|
||||
if (!roleUsuario) {
|
||||
return {
|
||||
sucesso: false,
|
||||
login: "",
|
||||
senha: "",
|
||||
mensagem: "Erro: Role 'usuario' não encontrada",
|
||||
};
|
||||
}
|
||||
|
||||
// Buscar um símbolo qualquer
|
||||
const simbolo = await ctx.db.query("simbolos").first();
|
||||
|
||||
if (!simbolo) {
|
||||
return {
|
||||
sucesso: false,
|
||||
login: "",
|
||||
senha: "",
|
||||
mensagem: "Erro: Nenhum símbolo encontrado. Crie um símbolo primeiro.",
|
||||
};
|
||||
}
|
||||
|
||||
// Criar funcionário
|
||||
const funcionarioId = await ctx.db.insert("funcionarios", {
|
||||
nome: nomeTeste,
|
||||
cpf: "111.222.333-44",
|
||||
rg: "1234567",
|
||||
nascimento: "1990-05-15",
|
||||
endereco: "Rua de Teste, 123",
|
||||
bairro: "Centro",
|
||||
cidade: "Recife",
|
||||
uf: "PE",
|
||||
cep: "50000-000",
|
||||
telefone: "(81) 98765-4321",
|
||||
email: emailTeste,
|
||||
matricula: loginTeste,
|
||||
admissaoData: "2023-01-15", // Admitido em jan/2023 (quase 2 anos)
|
||||
simboloId: simbolo._id,
|
||||
simboloTipo: simbolo.tipo,
|
||||
statusFerias: "ativo",
|
||||
regimeTrabalho: "clt", // CLT para testar
|
||||
descricaoCargo: "Analista Administrativo",
|
||||
nomePai: "José Silva",
|
||||
nomeMae: "Maria Silva",
|
||||
naturalidade: "Recife",
|
||||
naturalidadeUF: "PE",
|
||||
sexo: "masculino",
|
||||
estadoCivil: "solteiro",
|
||||
nacionalidade: "Brasileira",
|
||||
grauInstrucao: "superior",
|
||||
});
|
||||
|
||||
// Criar usuário
|
||||
const senhaHash = await hashPassword(senhaTeste);
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula: loginTeste,
|
||||
senhaHash,
|
||||
nome: nomeTeste,
|
||||
email: emailTeste,
|
||||
funcionarioId,
|
||||
roleId: roleUsuario._id,
|
||||
ativo: true,
|
||||
primeiroAcesso: false, // Já consideramos que fez primeiro acesso
|
||||
criadoEm: Date.now(),
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
|
||||
return {
|
||||
sucesso: true,
|
||||
login: loginTeste,
|
||||
senha: senhaTeste,
|
||||
mensagem: "Usuário de teste criado com sucesso!",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -25,5 +25,21 @@ crons.interval(
|
||||
{}
|
||||
);
|
||||
|
||||
// Criar períodos aquisitivos de férias automaticamente (diariamente)
|
||||
crons.interval(
|
||||
"criar-periodos-aquisitivos",
|
||||
{ hours: 24 },
|
||||
internal.saldoFerias.criarPeriodosAquisitivos,
|
||||
{}
|
||||
);
|
||||
|
||||
// Processar fila de emails pendentes a cada 2 minutos
|
||||
crons.interval(
|
||||
"processar-fila-emails",
|
||||
{ minutes: 2 },
|
||||
internal.email.processarFilaEmails,
|
||||
{}
|
||||
);
|
||||
|
||||
export default crons;
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ export const listarFilaEmails = query({
|
||||
)),
|
||||
limite: v.optional(v.number()),
|
||||
},
|
||||
returns: v.array(v.any()),
|
||||
handler: async (ctx, args) => {
|
||||
let query = ctx.db.query("notificacoesEmail");
|
||||
|
||||
@@ -140,9 +141,7 @@ export const reenviarEmail = mutation({
|
||||
});
|
||||
|
||||
/**
|
||||
* Action para enviar email (será implementado com nodemailer)
|
||||
*
|
||||
* NOTA: Este é um placeholder. Implementação real requer nodemailer.
|
||||
* Action para enviar email REAL usando nodemailer
|
||||
*/
|
||||
export const enviarEmailAction = action({
|
||||
args: {
|
||||
@@ -150,7 +149,9 @@ export const enviarEmailAction = action({
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
||||
handler: async (ctx, args) => {
|
||||
// TODO: Implementar com nodemailer quando instalado
|
||||
"use node";
|
||||
|
||||
const nodemailer = require("nodemailer");
|
||||
|
||||
try {
|
||||
// Buscar email da fila
|
||||
@@ -171,7 +172,11 @@ export const enviarEmailAction = action({
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
return { sucesso: false, erro: "Configuração de email não encontrada" };
|
||||
return { sucesso: false, erro: "Configuração de email não encontrada ou inativa" };
|
||||
}
|
||||
|
||||
if (!config.testado) {
|
||||
return { sucesso: false, erro: "Configuração SMTP não foi testada. Teste a conexão primeiro!" };
|
||||
}
|
||||
|
||||
// Marcar como enviando
|
||||
@@ -183,13 +188,33 @@ export const enviarEmailAction = action({
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Enviar email real com nodemailer aqui
|
||||
console.log("⚠️ AVISO: Envio de email simulado (nodemailer não instalado)");
|
||||
// Criar transporter do nodemailer
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: config.smtpHost,
|
||||
port: config.smtpPort,
|
||||
secure: config.smtpSecure, // true para 465, false para outros
|
||||
auth: {
|
||||
user: config.smtpUser,
|
||||
pass: config.smtpPassword,
|
||||
},
|
||||
tls: {
|
||||
// Não rejeitar certificados não autorizados (útil para testes)
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
// Enviar email REAL
|
||||
const info = await transporter.sendMail({
|
||||
from: `"${config.remetenteNome}" <${config.remetenteEmail}>`,
|
||||
to: email.destinatario,
|
||||
subject: email.assunto,
|
||||
html: email.corpo,
|
||||
});
|
||||
|
||||
console.log("✅ Email enviado com sucesso!");
|
||||
console.log(" Para:", email.destinatario);
|
||||
console.log(" Assunto:", email.assunto);
|
||||
|
||||
// Simular delay de envio
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.log(" Message ID:", info.messageId);
|
||||
|
||||
// Marcar como enviado
|
||||
await ctx.runMutation(async (ctx) => {
|
||||
@@ -201,6 +226,8 @@ export const enviarEmailAction = action({
|
||||
|
||||
return { sucesso: true };
|
||||
} catch (error: any) {
|
||||
console.error("❌ Erro ao enviar email:", error.message);
|
||||
|
||||
// Marcar como falha
|
||||
await ctx.runMutation(async (ctx) => {
|
||||
const email = await ctx.db.get(args.emailId);
|
||||
@@ -221,6 +248,7 @@ export const enviarEmailAction = action({
|
||||
*/
|
||||
export const processarFilaEmails = internalMutation({
|
||||
args: {},
|
||||
returns: v.object({ processados: v.number() }),
|
||||
handler: async (ctx) => {
|
||||
// Buscar emails pendentes (max 10 por execução)
|
||||
const emailsPendentes = await ctx.db
|
||||
@@ -240,17 +268,17 @@ export const processarFilaEmails = internalMutation({
|
||||
continue;
|
||||
}
|
||||
|
||||
// Agendar envio (será feito por uma action separada)
|
||||
// Por enquanto, apenas marca como enviado para não bloquear
|
||||
await ctx.db.patch(email._id, {
|
||||
status: "enviado",
|
||||
enviadoEm: Date.now(),
|
||||
// Agendar envio via action
|
||||
// IMPORTANTE: Não podemos chamar action diretamente de mutation
|
||||
// Por isso, usaremos o scheduler
|
||||
await ctx.scheduler.runAfter(0, "email:enviarEmailAction" as any, {
|
||||
emailId: email._id,
|
||||
});
|
||||
|
||||
processados++;
|
||||
}
|
||||
|
||||
console.log(`📧 Fila de emails processada: ${processados} emails`);
|
||||
console.log(`📧 Fila de emails processada: ${processados} emails agendados para envio`);
|
||||
|
||||
return { processados };
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query, internalMutation } from "./_generated/server";
|
||||
import { internal } from "./_generated/api";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
// Validador para períodos
|
||||
@@ -123,7 +124,7 @@ export const obterDetalhes = query({
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Criar solicitação de férias
|
||||
// Mutation: Criar solicitação de férias (com validação de saldo)
|
||||
export const criarSolicitacao = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
@@ -137,13 +138,22 @@ export const criarSolicitacao = mutation({
|
||||
throw new Error("É necessário adicionar pelo menos 1 período");
|
||||
}
|
||||
|
||||
if (args.periodos.length > 3) {
|
||||
throw new Error("Máximo de 3 períodos permitidos");
|
||||
}
|
||||
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario) throw new Error("Funcionário não encontrado");
|
||||
|
||||
// Calcular total de dias
|
||||
let totalDias = 0;
|
||||
for (const p of args.periodos) {
|
||||
totalDias += p.diasCorridos;
|
||||
}
|
||||
|
||||
// Reservar dias no saldo (impede uso duplo)
|
||||
await ctx.runMutation(internal.saldoFerias.reservarDias, {
|
||||
funcionarioId: args.funcionarioId,
|
||||
anoReferencia: args.anoReferencia,
|
||||
totalDias,
|
||||
});
|
||||
|
||||
// Buscar usuário que está criando (pode não ser o próprio funcionário)
|
||||
const usuario = await ctx.db
|
||||
.query("usuarios")
|
||||
@@ -209,6 +219,11 @@ export const aprovar = mutation({
|
||||
],
|
||||
});
|
||||
|
||||
// Atualizar saldo (de pendente para usado)
|
||||
await ctx.runMutation(internal.saldoFerias.atualizarSaldoAposAprovacao, {
|
||||
solicitacaoId: args.solicitacaoId,
|
||||
});
|
||||
|
||||
// Notificar funcionário
|
||||
if (funcionario) {
|
||||
const usuario = await ctx.db
|
||||
@@ -264,6 +279,11 @@ export const reprovar = mutation({
|
||||
],
|
||||
});
|
||||
|
||||
// Liberar dias reservados de volta ao saldo
|
||||
await ctx.runMutation(internal.saldoFerias.liberarDias, {
|
||||
solicitacaoId: args.solicitacaoId,
|
||||
});
|
||||
|
||||
// Notificar funcionário
|
||||
if (funcionario) {
|
||||
const usuario = await ctx.db
|
||||
@@ -306,11 +326,24 @@ export const ajustarEAprovar = mutation({
|
||||
throw new Error("É necessário adicionar pelo menos 1 período");
|
||||
}
|
||||
|
||||
if (args.novosPeriodos.length > 3) {
|
||||
throw new Error("Máximo de 3 períodos permitidos");
|
||||
const funcionario = await ctx.db.get(solicitacao.funcionarioId);
|
||||
|
||||
// Liberar dias antigos
|
||||
await ctx.runMutation(internal.saldoFerias.liberarDias, {
|
||||
solicitacaoId: args.solicitacaoId,
|
||||
});
|
||||
|
||||
// Calcular novos dias e reservar
|
||||
let totalNovosDias = 0;
|
||||
for (const p of args.novosPeriodos) {
|
||||
totalNovosDias += p.diasCorridos;
|
||||
}
|
||||
|
||||
const funcionario = await ctx.db.get(solicitacao.funcionarioId);
|
||||
await ctx.runMutation(internal.saldoFerias.reservarDias, {
|
||||
funcionarioId: solicitacao.funcionarioId,
|
||||
anoReferencia: solicitacao.anoReferencia,
|
||||
totalDias: totalNovosDias,
|
||||
});
|
||||
|
||||
await ctx.db.patch(args.solicitacaoId, {
|
||||
status: "data_ajustada_aprovada",
|
||||
@@ -328,6 +361,11 @@ export const ajustarEAprovar = mutation({
|
||||
],
|
||||
});
|
||||
|
||||
// Atualizar saldo (marcar como usado)
|
||||
await ctx.runMutation(internal.saldoFerias.atualizarSaldoAposAprovacao, {
|
||||
solicitacaoId: args.solicitacaoId,
|
||||
});
|
||||
|
||||
// Notificar funcionário
|
||||
if (funcionario) {
|
||||
const usuario = await ctx.db
|
||||
|
||||
@@ -12,6 +12,7 @@ const aposentadoValidator = v.optional(v.union(v.literal("nao"), v.literal("funa
|
||||
|
||||
export const getAll = query({
|
||||
args: {},
|
||||
returns: v.array(v.any()),
|
||||
handler: async (ctx) => {
|
||||
const funcionarios = await ctx.db.query("funcionarios").collect();
|
||||
// Retornar apenas os campos necessários para listagem
|
||||
@@ -39,6 +40,7 @@ export const getAll = query({
|
||||
|
||||
export const getById = query({
|
||||
args: { id: v.id("funcionarios") },
|
||||
returns: v.union(v.any(), v.null()),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.id);
|
||||
},
|
||||
@@ -301,6 +303,7 @@ export const remove = mutation({
|
||||
// Query para obter ficha completa para impressão
|
||||
export const getFichaCompleta = query({
|
||||
args: { id: v.id("funcionarios") },
|
||||
returns: v.union(v.any(), v.null()),
|
||||
handler: async (ctx, args) => {
|
||||
const funcionario = await ctx.db.get(args.id);
|
||||
if (!funcionario) {
|
||||
|
||||
@@ -288,3 +288,4 @@ export const verificarNiveisIncorretos = query({
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -208,3 +208,4 @@ export const removerAdminAntigo = internalMutation({
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
556
packages/backend/convex/saldoFerias.ts
Normal file
556
packages/backend/convex/saldoFerias.ts
Normal file
@@ -0,0 +1,556 @@
|
||||
import { v } from "convex/values";
|
||||
import { query, mutation, internalMutation } from "./_generated/server";
|
||||
import { internal } from "./_generated/api";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
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: 3,
|
||||
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: any, 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, criar automaticamente
|
||||
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);
|
||||
|
||||
// Criar período aquisitivo
|
||||
await ctx.db.insert("periodosAquisitivos", {
|
||||
funcionarioId: args.funcionarioId,
|
||||
anoReferencia: args.anoReferencia,
|
||||
dataInicio,
|
||||
dataFim,
|
||||
diasDireito: 30,
|
||||
diasUsados: 0,
|
||||
diasPendentes: 0,
|
||||
diasDisponiveis: 30,
|
||||
abonoPermitido: config.abonoPermitido,
|
||||
diasAbono: 0,
|
||||
status: "ativo",
|
||||
});
|
||||
|
||||
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") {
|
||||
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;
|
||||
},
|
||||
});
|
||||
@@ -36,6 +36,14 @@ export default defineSchema({
|
||||
v.literal("ativo"),
|
||||
v.literal("em_ferias")
|
||||
)),
|
||||
|
||||
// Regime de trabalho (para cálculo correto de férias)
|
||||
regimeTrabalho: v.optional(v.union(
|
||||
v.literal("clt"), // CLT - Consolidação das Leis do Trabalho
|
||||
v.literal("estatutario_pe"), // Servidor Público Estadual de Pernambuco
|
||||
v.literal("estatutario_federal"), // Servidor Público Federal
|
||||
v.literal("estatutario_municipal") // Servidor Público Municipal
|
||||
)),
|
||||
|
||||
// Dados Pessoais Adicionais (opcionais)
|
||||
nomePai: v.optional(v.string()),
|
||||
@@ -207,6 +215,28 @@ export default defineSchema({
|
||||
.index("by_destinatario", ["destinatarioId"])
|
||||
.index("by_destinatario_and_lida", ["destinatarioId", "lida"]),
|
||||
|
||||
// Períodos aquisitivos e saldos de férias
|
||||
periodosAquisitivos: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
anoReferencia: v.number(), // Ano do período aquisitivo (ex: 2024)
|
||||
dataInicio: v.string(), // Data de início do período aquisitivo
|
||||
dataFim: v.string(), // Data de fim do período aquisitivo
|
||||
diasDireito: v.number(), // Dias de férias que tem direito (30 ou proporcional)
|
||||
diasUsados: v.number(), // Dias já usados
|
||||
diasPendentes: v.number(), // Dias em solicitações aguardando aprovação
|
||||
diasDisponiveis: v.number(), // Dias disponíveis = direito - usados - pendentes
|
||||
abonoPermitido: v.boolean(), // Se pode vender 1/3 das férias
|
||||
diasAbono: v.number(), // Dias vendidos como abono pecuniário
|
||||
status: v.union(
|
||||
v.literal("ativo"), // Período vigente
|
||||
v.literal("vencido"), // Período vencido (não tirou férias)
|
||||
v.literal("concluido") // Período totalmente utilizado
|
||||
),
|
||||
})
|
||||
.index("by_funcionario", ["funcionarioId"])
|
||||
.index("by_funcionario_and_ano", ["funcionarioId", "anoReferencia"])
|
||||
.index("by_funcionario_and_status", ["funcionarioId", "status"]),
|
||||
|
||||
times: defineTable({
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
@@ -304,7 +334,8 @@ export default defineSchema({
|
||||
.index("by_role", ["roleId"])
|
||||
.index("by_ativo", ["ativo"])
|
||||
.index("by_status_presenca", ["statusPresenca"])
|
||||
.index("by_bloqueado", ["bloqueado"]),
|
||||
.index("by_bloqueado", ["bloqueado"])
|
||||
.index("by_funcionarioId", ["funcionarioId"]),
|
||||
|
||||
roles: defineTable({
|
||||
nome: v.string(), // "admin", "ti_master", "ti_usuario", "usuario_avancado", "usuario"
|
||||
|
||||
@@ -4,6 +4,60 @@ import { hashPassword, generateToken } from "./auth/utils";
|
||||
import { registrarAtividade } from "./logsAtividades";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
/**
|
||||
* Associar funcionário a um usuário
|
||||
*/
|
||||
export const associarFuncionario = mutation({
|
||||
args: {
|
||||
usuarioId: v.id("usuarios"),
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean() }),
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se o funcionário existe
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario) {
|
||||
throw new Error("Funcionário não encontrado");
|
||||
}
|
||||
|
||||
// Verificar se o funcionário já está associado a outro usuário
|
||||
const usuarioExistente = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", args.funcionarioId))
|
||||
.first();
|
||||
|
||||
if (usuarioExistente && usuarioExistente._id !== args.usuarioId) {
|
||||
throw new Error(
|
||||
`Este funcionário já está associado ao usuário: ${usuarioExistente.nome} (${usuarioExistente.matricula})`
|
||||
);
|
||||
}
|
||||
|
||||
// Associar funcionário ao usuário
|
||||
await ctx.db.patch(args.usuarioId, {
|
||||
funcionarioId: args.funcionarioId,
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Desassociar funcionário de um usuário
|
||||
*/
|
||||
export const desassociarFuncionario = mutation({
|
||||
args: {
|
||||
usuarioId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean() }),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.usuarioId, {
|
||||
funcionarioId: undefined,
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Criar novo usuário (apenas TI)
|
||||
*/
|
||||
@@ -405,6 +459,30 @@ export const atualizarPerfil = mutation({
|
||||
*/
|
||||
export const obterPerfil = query({
|
||||
args: {},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id("usuarios"),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
matricula: v.string(),
|
||||
funcionarioId: v.optional(v.id("funcionarios")),
|
||||
avatar: v.optional(v.string()),
|
||||
fotoPerfil: v.optional(v.id("_storage")),
|
||||
fotoPerfilUrl: v.union(v.string(), v.null()),
|
||||
setor: v.optional(v.string()),
|
||||
statusMensagem: v.optional(v.string()),
|
||||
statusPresenca: v.optional(v.union(
|
||||
v.literal("online"),
|
||||
v.literal("offline"),
|
||||
v.literal("ausente"),
|
||||
v.literal("externo"),
|
||||
v.literal("em_reuniao")
|
||||
)),
|
||||
notificacoesAtivadas: v.boolean(),
|
||||
somNotificacao: v.boolean(),
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
console.log("=== DEBUG obterPerfil ===");
|
||||
|
||||
@@ -464,6 +542,7 @@ export const obterPerfil = query({
|
||||
nome: usuarioAtual.nome,
|
||||
email: usuarioAtual.email,
|
||||
matricula: usuarioAtual.matricula,
|
||||
funcionarioId: usuarioAtual.funcionarioId,
|
||||
avatar: usuarioAtual.avatar,
|
||||
fotoPerfil: usuarioAtual.fotoPerfil,
|
||||
fotoPerfilUrl,
|
||||
|
||||
@@ -99,3 +99,4 @@ export const removerDuplicatas = internalMutation({
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user