Files
sgse-app/packages/backend/convex/ferias.ts

775 lines
22 KiB
TypeScript

import { v } from 'convex/values';
import { internal } from './_generated/api';
import type { Doc, Id } from './_generated/dataModel';
import { internalMutation, mutation, query } from './_generated/server';
// Validador para períodos
const periodoValidator = v.object({
dataInicio: v.string(),
dataFim: v.string(),
diasCorridos: v.number()
});
// 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;
return diffDays;
}
// Helper: Agrupar registros de ferias por funcionarioId + anoReferencia
function agruparPorSolicitacao(registros: Array<Doc<'ferias'>>): Array<{
funcionarioId: Id<'funcionarios'>;
anoReferencia: number;
periodos: Array<Doc<'ferias'>>;
status: string;
observacao?: string;
motivoReprovacao?: string;
gestorId?: Id<'usuarios'>;
dataAprovacao?: number;
dataReprovacao?: number;
historicoAlteracoes?: Array<{
data: number;
usuarioId: Id<'usuarios'>;
acao: string;
}>;
}> {
const grupos = new Map<string, Array<Doc<'ferias'>>>();
for (const registro of registros) {
const chave = `${registro.funcionarioId}_${registro.anoReferencia}`;
if (!grupos.has(chave)) {
grupos.set(chave, []);
}
grupos.get(chave)!.push(registro);
}
return Array.from(grupos.entries()).map(([_, periodos]) => {
// Ordenar por data de criação para manter ordem
periodos.sort((a, b) => a._creationTime - b._creationTime);
// Pegar informações da primeira solicitação (todos têm os mesmos campos compartilhados)
const primeiro = periodos[0];
return {
funcionarioId: primeiro.funcionarioId,
anoReferencia: primeiro.anoReferencia,
periodos,
status: primeiro.status,
observacao: primeiro.observacao,
motivoReprovacao: primeiro.motivoReprovacao,
gestorId: primeiro.gestorId,
dataAprovacao: primeiro.dataAprovacao,
dataReprovacao: primeiro.dataReprovacao,
historicoAlteracoes: primeiro.historicoAlteracoes
};
});
}
// Query: Listar TODAS as solicitações (para RH) - períodos individuais
export const listarTodas = query({
args: {},
handler: async (ctx) => {
const todasFerias = await ctx.db.query('ferias').collect();
const periodosComDetalhes = await Promise.all(
todasFerias.map(async (ferias) => {
const funcionario = await ctx.db.get(ferias.funcionarioId);
// Buscar usuário do funcionário para obter fotoPerfilUrl
let fotoPerfilUrl: string | null = null;
if (funcionario) {
const usuario = await ctx.db
.query('usuarios')
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id))
.first();
if (usuario?.fotoPerfil) {
fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil);
}
}
// Buscar time do funcionário
const membroTime = await ctx.db
.query('timesMembros')
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', ferias.funcionarioId))
.filter((q) => q.eq(q.field('ativo'), true))
.first();
let time: Doc<'times'> | null = null;
let gestor: { _id: Id<'usuarios'>; nome: string } | null = null;
if (membroTime) {
time = await ctx.db.get(membroTime.timeId);
// Buscar gestor do time
if (time?.gestorId) {
const gestorUsuario = await ctx.db.get(time.gestorId);
if (gestorUsuario?.funcionarioId) {
// Buscar funcionário do gestor para obter o nome
const gestorFuncionario = await ctx.db.get(gestorUsuario.funcionarioId);
if (gestorFuncionario) {
gestor = {
_id: gestorUsuario._id,
nome: gestorFuncionario.nome
};
}
}
}
}
return {
...ferias,
funcionario: funcionario
? {
...funcionario,
fotoPerfilUrl
}
: null,
time,
gestor
};
})
);
return periodosComDetalhes.sort((a, b) => b._creationTime - a._creationTime);
}
});
// Query: Listar solicitações do funcionário - períodos individuais
export const listarMinhasSolicitacoes = query({
args: { funcionarioId: v.id('funcionarios') },
handler: async (ctx, args) => {
const todasFerias = await ctx.db
.query('ferias')
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId))
.collect();
const funcionario = await ctx.db.get(args.funcionarioId);
// Buscar time do funcionário
const membroTime = await ctx.db
.query('timesMembros')
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId))
.filter((q) => q.eq(q.field('ativo'), true))
.first();
let time = null;
if (membroTime) {
time = await ctx.db.get(membroTime.timeId);
}
// Retornar períodos individuais com detalhes
return todasFerias
.map((ferias) => ({
...ferias,
funcionario,
time
}))
.sort((a, b) => b._creationTime - a._creationTime);
}
});
// Query: Listar solicitações dos subordinados (para gestores) - períodos individuais
export const listarSolicitacoesSubordinados = query({
args: { gestorId: v.id('usuarios') },
handler: async (ctx, args) => {
// Buscar times onde o usuário é gestor
const timesGestor = await ctx.db
.query('times')
.withIndex('by_gestor', (q) => q.eq('gestorId', args.gestorId))
.filter((q) => q.eq(q.field('ativo'), true))
.collect();
const todasFerias: Array<Doc<'ferias'>> = [];
for (const time of timesGestor) {
// Buscar membros do time
const membros = await ctx.db
.query('timesMembros')
.withIndex('by_time_and_ativo', (q) => q.eq('timeId', time._id).eq('ativo', true))
.collect();
// Buscar férias de cada membro
for (const membro of membros) {
const ferias = await ctx.db
.query('ferias')
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', membro.funcionarioId))
.collect();
todasFerias.push(...ferias);
}
}
// Adicionar info do funcionário e time para cada período
const periodosComDetalhes = await Promise.all(
todasFerias.map(async (ferias) => {
const funcionario = await ctx.db.get(ferias.funcionarioId);
// Buscar time do funcionário
const membroTime = await ctx.db
.query('timesMembros')
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', ferias.funcionarioId))
.filter((q) => q.eq(q.field('ativo'), true))
.first();
let time = null;
if (membroTime) {
time = await ctx.db.get(membroTime.timeId);
}
return {
...ferias,
funcionario,
time
};
})
);
return periodosComDetalhes.sort((a, b) => b._creationTime - a._creationTime);
}
});
// Query: Obter detalhes de um período individual
export const obterDetalhes = query({
args: {
feriasId: v.id('ferias')
},
handler: async (ctx, args) => {
const ferias = await ctx.db.get(args.feriasId);
if (!ferias) return null;
const funcionario = await ctx.db.get(ferias.funcionarioId);
let gestor = null;
if (ferias.gestorId) {
gestor = await ctx.db.get(ferias.gestorId);
}
// Buscar time do funcionário
const membroTime = await ctx.db
.query('timesMembros')
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', ferias.funcionarioId))
.filter((q) => q.eq(q.field('ativo'), true))
.first();
let time = null;
if (membroTime) {
time = await ctx.db.get(membroTime.timeId);
}
return {
...ferias,
funcionario,
gestor,
time
};
}
});
// Mutation: Criar solicitação de férias (cria um registro por período)
export const criarSolicitacao = mutation({
args: {
funcionarioId: v.id('funcionarios'),
anoReferencia: v.number(),
periodos: v.array(periodoValidator),
observacao: v.optional(v.string())
},
returns: v.array(v.id('ferias')),
handler: async (ctx, args) => {
if (args.periodos.length === 0) {
throw new Error('É necessário adicionar pelo menos 1 período');
}
const funcionario = await ctx.db.get(args.funcionarioId);
if (!funcionario) throw new Error('Funcionário não encontrado');
// Buscar usuário que está criando (pode não ser o próprio funcionário)
const usuario = await ctx.db
.query('usuarios')
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', args.funcionarioId))
.first();
const historicoInicial = [
{
data: Date.now(),
usuarioId: usuario?._id || funcionario.gestorId!,
acao: 'Solicitação criada'
}
];
// Criar um registro para cada período
const idsCriados: Array<Id<'ferias'>> = [];
for (const periodo of args.periodos) {
const feriasId = await ctx.db.insert('ferias', {
funcionarioId: args.funcionarioId,
anoReferencia: args.anoReferencia,
dataInicio: periodo.dataInicio,
dataFim: periodo.dataFim,
diasFerias: periodo.diasCorridos,
status: 'aguardando_aprovacao',
observacao: args.observacao,
diasAbono: 0,
historicoAlteracoes: historicoInicial
});
idsCriados.push(feriasId);
}
// Notificar gestor (usar o primeiro ID criado)
if (funcionario.gestorId && idsCriados.length > 0) {
await ctx.db.insert('notificacoesFerias', {
destinatarioId: funcionario.gestorId,
feriasId: idsCriados[0],
tipo: 'nova_solicitacao',
lida: false,
mensagem: `${funcionario.nome} solicitou férias`
});
}
return idsCriados;
}
});
// Mutation: Aprovar período de férias individual
export const aprovar = mutation({
args: {
feriasId: v.id('ferias'),
gestorId: v.id('usuarios')
},
returns: v.null(),
handler: async (ctx, args) => {
// Buscar o registro específico
const registro = await ctx.db.get(args.feriasId);
if (!registro) {
throw new Error('Período de férias não encontrado');
}
// Verificar se está aguardando aprovação
if (registro.status !== 'aguardando_aprovacao') {
throw new Error('Este período já foi processado');
}
const funcionario = await ctx.db.get(registro.funcionarioId);
// Atualizar o registro
await ctx.db.patch(registro._id, {
status: 'aprovado',
gestorId: args.gestorId,
dataAprovacao: Date.now(),
historicoAlteracoes: [
...(registro.historicoAlteracoes || []),
{
data: Date.now(),
usuarioId: args.gestorId,
acao: 'Aprovado'
}
]
});
// Notificar funcionário
if (funcionario) {
const usuario = await ctx.db
.query('usuarios')
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id))
.first();
if (usuario) {
await ctx.db.insert('notificacoesFerias', {
destinatarioId: usuario._id,
feriasId: registro._id,
tipo: 'aprovado',
lida: false,
mensagem: `Período de férias de ${registro.diasFerias} dias foi aprovado!`
});
}
}
return null;
}
});
// Mutation: Reprovar período de férias individual
export const reprovar = mutation({
args: {
feriasId: v.id('ferias'),
gestorId: v.id('usuarios'),
motivoReprovacao: v.string()
},
returns: v.null(),
handler: async (ctx, args) => {
// Buscar o registro específico
const registro = await ctx.db.get(args.feriasId);
if (!registro) {
throw new Error('Período de férias não encontrado');
}
// Verificar se está aguardando aprovação
if (registro.status !== 'aguardando_aprovacao') {
throw new Error('Este período já foi processado');
}
const funcionario = await ctx.db.get(registro.funcionarioId);
// Atualizar o registro
await ctx.db.patch(registro._id, {
status: 'reprovado',
gestorId: args.gestorId,
dataReprovacao: Date.now(),
motivoReprovacao: args.motivoReprovacao,
historicoAlteracoes: [
...(registro.historicoAlteracoes || []),
{
data: Date.now(),
usuarioId: args.gestorId,
acao: `Reprovado: ${args.motivoReprovacao}`
}
]
});
// Notificar funcionário
if (funcionario) {
const usuario = await ctx.db
.query('usuarios')
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id))
.first();
if (usuario) {
await ctx.db.insert('notificacoesFerias', {
destinatarioId: usuario._id,
feriasId: registro._id,
tipo: 'reprovado',
lida: false,
mensagem: `Período de férias de ${registro.diasFerias} dias foi reprovado: ${args.motivoReprovacao}`
});
}
}
return null;
}
});
// Mutation: Ajustar data e aprovar período individual
export const ajustarEAprovar = mutation({
args: {
feriasId: v.id('ferias'),
gestorId: v.id('usuarios'),
novaDataInicio: v.string(),
novaDataFim: v.string()
},
returns: v.null(),
handler: async (ctx, args) => {
// Buscar o registro específico
const registroAntigo = await ctx.db.get(args.feriasId);
if (!registroAntigo) {
throw new Error('Período de férias não encontrado');
}
// Verificar se está aguardando aprovação
if (registroAntigo.status !== 'aguardando_aprovacao') {
throw new Error('Este período já foi processado');
}
const funcionario = await ctx.db.get(registroAntigo.funcionarioId);
// Calcular novos dias
const novosDias = calcularDiasEntreDatas(args.novaDataInicio, args.novaDataFim);
// Atualizar o registro com novas datas
await ctx.db.patch(registroAntigo._id, {
dataInicio: args.novaDataInicio,
dataFim: args.novaDataFim,
diasFerias: novosDias,
status: 'data_ajustada_aprovada',
gestorId: args.gestorId,
dataAprovacao: Date.now(),
historicoAlteracoes: [
...(registroAntigo.historicoAlteracoes || []),
{
data: Date.now(),
usuarioId: args.gestorId,
acao: `Data ajustada e aprovada: ${registroAntigo.dataInicio} - ${registroAntigo.dataFim}${args.novaDataInicio} - ${args.novaDataFim}`
}
]
});
// Notificar funcionário
if (funcionario) {
const usuario = await ctx.db
.query('usuarios')
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id))
.first();
if (usuario) {
await ctx.db.insert('notificacoesFerias', {
destinatarioId: usuario._id,
feriasId: registroAntigo._id,
tipo: 'data_ajustada',
lida: false,
mensagem: `Período de férias foi aprovado com ajuste de datas: ${args.novaDataInicio} a ${args.novaDataFim}`
});
}
}
return null;
}
});
// Query: Verificar status de férias automático
export const verificarStatusFerias = query({
args: { funcionarioId: v.id('funcionarios') },
returns: v.union(v.literal('ativo'), v.literal('em_ferias')),
handler: async (ctx, args) => {
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
const feriasAprovadas = await ctx.db
.query('ferias')
.withIndex('by_funcionario_and_status', (q) =>
q.eq('funcionarioId', args.funcionarioId).eq('status', 'aprovado')
)
.collect();
const feriasAjustadas = await ctx.db
.query('ferias')
.withIndex('by_funcionario_and_status', (q) =>
q.eq('funcionarioId', args.funcionarioId).eq('status', 'data_ajustada_aprovada')
)
.collect();
const feriasEmFerias = await ctx.db
.query('ferias')
.withIndex('by_funcionario_and_status', (q) =>
q.eq('funcionarioId', args.funcionarioId).eq('status', 'EmFérias')
)
.collect();
const todasFerias = [...feriasAprovadas, ...feriasAjustadas, ...feriasEmFerias];
for (const ferias of todasFerias) {
const inicio = new Date(ferias.dataInicio);
const fim = new Date(ferias.dataFim);
inicio.setHours(0, 0, 0, 0);
fim.setHours(23, 59, 59, 999);
if (hoje >= inicio && hoje <= fim) {
return 'em_ferias';
}
}
return 'ativo';
}
});
// Query: Obter notificações não lidas
export const obterNotificacoesNaoLidas = query({
args: { usuarioId: v.id('usuarios') },
handler: async (ctx, args) => {
return await ctx.db
.query('notificacoesFerias')
.withIndex('by_destinatario_and_lida', (q) =>
q.eq('destinatarioId', args.usuarioId).eq('lida', false)
)
.collect();
}
});
// Mutation: Marcar notificação como lida
export const marcarComoLida = mutation({
args: { notificacaoId: v.id('notificacoesFerias') },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.notificacaoId, { lida: true });
return null;
}
});
// Mutation: Atualizar status de um período individual
export const atualizarStatus = mutation({
args: {
feriasId: v.id('ferias'),
novoStatus: v.union(
v.literal('aguardando_aprovacao'),
v.literal('aprovado'),
v.literal('reprovado'),
v.literal('data_ajustada_aprovada'),
v.literal('Cancelado_RH')
),
usuarioId: v.id('usuarios')
},
returns: v.null(),
handler: async (ctx, args) => {
// Buscar o registro específico
const registro = await ctx.db.get(args.feriasId);
if (!registro) {
throw new Error('Período de férias não encontrado');
}
// Atualizar status e histórico
const acao = `Status alterado para ${args.novoStatus}`;
const updateData: {
status: typeof args.novoStatus;
historicoAlteracoes: Array<{
data: number;
usuarioId: Id<'usuarios'>;
acao: string;
}>;
gestorId?: undefined;
dataAprovacao?: undefined;
dataReprovacao?: undefined;
motivoReprovacao?: undefined;
} = {
status: args.novoStatus,
historicoAlteracoes: [
...(registro.historicoAlteracoes || []),
{
data: Date.now(),
usuarioId: args.usuarioId,
acao
}
]
};
// Se voltar para aguardando_aprovacao, limpar campos relacionados
if (args.novoStatus === 'aguardando_aprovacao') {
await ctx.db.patch(registro._id, {
...updateData,
gestorId: undefined,
dataAprovacao: undefined,
dataReprovacao: undefined,
motivoReprovacao: undefined
});
} else {
await ctx.db.patch(registro._id, updateData);
}
return null;
}
});
// Internal Mutation: Atualizar status de todos os funcionários
export const atualizarStatusTodosFuncionarios = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
const funcionarios = await ctx.db.query('funcionarios').collect();
for (const func of funcionarios) {
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
// Buscar todos os registros de férias que podem estar em férias
// Buscar por status específico para criar mapas de referência
const feriasAprovadas = await ctx.db
.query('ferias')
.withIndex('by_funcionario_and_status', (q) =>
q.eq('funcionarioId', func._id).eq('status', 'aprovado')
)
.collect();
const feriasAjustadas = await ctx.db
.query('ferias')
.withIndex('by_funcionario_and_status', (q) =>
q.eq('funcionarioId', func._id).eq('status', 'data_ajustada_aprovada')
)
.collect();
const feriasEmFerias = await ctx.db
.query('ferias')
.withIndex('by_funcionario_and_status', (q) =>
q.eq('funcionarioId', func._id).eq('status', 'EmFérias')
)
.collect();
// Criar mapas para verificar status original
// Quando um registro está "EmFérias", precisamos saber qual era o status anterior
// Vamos usar o histórico ou verificar se o ID estava nas listas antes
const idsAprovados = new Set(feriasAprovadas.map((f) => f._id));
const idsAjustados = new Set(feriasAjustadas.map((f) => f._id));
// Para registros que estão "EmFérias", verificar o histórico para determinar status anterior
// Se não houver histórico claro, usar lógica: se foi aprovado recentemente, provavelmente era "aprovado"
// Por enquanto, vamos usar uma heurística: se o registro está "EmFérias" e não está nas listas,
// vamos verificar o histórico de alterações para encontrar o status anterior
const statusAnteriorPorId = new Map<Id<'ferias'>, 'aprovado' | 'data_ajustada_aprovada'>();
for (const ferias of feriasEmFerias) {
// Verificar histórico para encontrar status anterior
if (ferias.historicoAlteracoes && ferias.historicoAlteracoes.length > 0) {
// Procurar pela última alteração que mudou para "EmFérias" ou antes disso
const historico = ferias.historicoAlteracoes;
for (let i = historico.length - 1; i >= 0; i--) {
const entrada = historico[i];
if (entrada.acao.includes('Aprovado') || entrada.acao.includes('aprovado')) {
statusAnteriorPorId.set(ferias._id, 'aprovado');
break;
} else if (
entrada.acao.includes('Data ajustada') ||
entrada.acao.includes('ajustada')
) {
statusAnteriorPorId.set(ferias._id, 'data_ajustada_aprovada');
break;
}
}
}
// Se não encontrou no histórico, usar fallback: assumir "aprovado"
if (!statusAnteriorPorId.has(ferias._id)) {
statusAnteriorPorId.set(ferias._id, 'aprovado');
}
}
// Combinar todos os registros
const todasFerias = [...feriasAprovadas, ...feriasAjustadas, ...feriasEmFerias];
let emFerias = false;
for (const ferias of todasFerias) {
const inicio = new Date(ferias.dataInicio);
const fim = new Date(ferias.dataFim);
inicio.setHours(0, 0, 0, 0);
fim.setHours(23, 59, 59, 999);
if (hoje >= inicio && hoje <= fim) {
emFerias = true;
// Atualizar status para "EmFérias" se ainda não estiver
if (ferias.status !== 'EmFérias') {
await ctx.db.patch(ferias._id, {
status: 'EmFérias'
});
}
} else {
// Se saiu do período e está "EmFérias", voltar para o status anterior
if (ferias.status === 'EmFérias') {
// Determinar status anterior
let statusAnterior: 'aprovado' | 'data_ajustada_aprovada';
if (idsAprovados.has(ferias._id)) {
statusAnterior = 'aprovado';
} else if (idsAjustados.has(ferias._id)) {
statusAnterior = 'data_ajustada_aprovada';
} else {
// Usar histórico ou fallback
statusAnterior = statusAnteriorPorId.get(ferias._id) || 'aprovado';
}
await ctx.db.patch(ferias._id, {
status: statusAnterior
});
}
}
}
const novoStatus = emFerias ? 'em_ferias' : 'ativo';
if (func.statusFerias !== novoStatus) {
await ctx.db.patch(func._id, { statusFerias: novoStatus });
}
}
return null;
}
});