import { v } from 'convex/values'; import { mutation, query } from './_generated/server'; import { Id } from './_generated/dataModel'; import type { QueryCtx, MutationCtx } from './_generated/server'; import { registrarAtividade } from './logsAtividades'; import { getCurrentUserFunction } from './auth'; import { internal } from './_generated/api'; // ========== HELPERS ========== /** * Helper function para obter usuário autenticado */ async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) { const usuario = await getCurrentUserFunction(ctx); return usuario ?? null; } /** * Helper para calcular dias entre duas datas */ function calcularDias(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 para recalcular banco de horas em um período */ async function recalcularBancoHorasPeriodo( ctx: MutationCtx, funcionarioId: Id<'funcionarios'>, dataInicio: string, dataFim: string ): Promise { // Gerar todas as datas do período const dataInicioObj = new Date(dataInicio); const dataFimObj = new Date(dataFim); const datas: string[] = []; const dataAtual = new Date(dataInicioObj); while (dataAtual <= dataFimObj) { const ano = dataAtual.getFullYear(); const mes = String(dataAtual.getMonth() + 1).padStart(2, '0'); const dia = String(dataAtual.getDate()).padStart(2, '0'); datas.push(`${ano}-${mes}-${dia}`); dataAtual.setDate(dataAtual.getDate() + 1); } // Recalcular para cada data usando a mutation interna (agendar para execução assíncrona) for (let i = 0; i < datas.length; i++) { await ctx.scheduler.runAfter(i * 100, internal.pontos.recalcularBancoHorasData, { funcionarioId, data: datas[i]! }); } } // ========== QUERIES ========== /** * Listar todos os atestados e licenças com detalhes do funcionário */ export const listarTodos = query({ args: {}, handler: async (ctx) => { try { const [atestados, licencas] = await Promise.all([ ctx.db.query('atestados').collect(), ctx.db.query('licencas').collect() ]); const atestadosComDetalhes = await Promise.all( atestados.map(async (a) => { try { const funcionario = await ctx.db.get(a.funcionarioId); const criadoPor = await ctx.db.get(a.criadoPor); // Buscar foto do perfil do funcionário através do usuário associado 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); } } return { ...a, funcionario, fotoPerfilUrl, criadoPorNome: criadoPor?.nome || 'Sistema', dias: calcularDias(a.dataInicio, a.dataFim), status: new Date(a.dataFim) >= new Date() ? 'ativo' : 'finalizado' }; } catch (error) { console.error('Erro ao buscar detalhes do atestado:', error); return { ...a, funcionario: null, fotoPerfilUrl: null, criadoPorNome: 'Sistema', dias: calcularDias(a.dataInicio, a.dataFim), status: new Date(a.dataFim) >= new Date() ? 'ativo' : 'finalizado' }; } }) ); const licencasComDetalhes = await Promise.all( licencas.map(async (l) => { try { const funcionario = await ctx.db.get(l.funcionarioId); const criadoPor = await ctx.db.get(l.criadoPor); const licencaOriginal = l.licencaOriginalId ? await ctx.db.get(l.licencaOriginalId) : null; // Buscar foto do perfil do funcionário através do usuário associado 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); } } return { ...l, funcionario, fotoPerfilUrl, criadoPorNome: criadoPor?.nome || 'Sistema', licencaOriginal, dias: calcularDias(l.dataInicio, l.dataFim), status: new Date(l.dataFim) >= new Date() ? 'ativo' : 'finalizado' }; } catch (error) { console.error('Erro ao buscar detalhes da licença:', error); return { ...l, funcionario: null, fotoPerfilUrl: null, criadoPorNome: 'Sistema', licencaOriginal: null, dias: calcularDias(l.dataInicio, l.dataFim), status: new Date(l.dataFim) >= new Date() ? 'ativo' : 'finalizado' }; } }) ); return { atestados: atestadosComDetalhes.sort((a, b) => b._creationTime - a._creationTime), licencas: licencasComDetalhes.sort((a, b) => b._creationTime - a._creationTime) }; } catch (error) { console.error('Erro em listarTodos:', error); return { atestados: [], licencas: [] }; } } }); /** * Listar por funcionário específico */ export const listarPorFuncionario = query({ args: { funcionarioId: v.id('funcionarios') }, handler: async (ctx, args) => { const [atestados, licencas] = await Promise.all([ ctx.db .query('atestados') .withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId)) .collect(), ctx.db .query('licencas') .withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId)) .collect() ]); return { atestados: atestados.sort((a, b) => b._creationTime - a._creationTime), licencas: licencas.sort((a, b) => b._creationTime - a._creationTime) }; } }); /** * Listar por período */ export const listarPorPeriodo = query({ args: { dataInicio: v.string(), dataFim: v.string() }, handler: async (ctx, args) => { const dataInicioObj = new Date(args.dataInicio); const dataFimObj = new Date(args.dataFim); const atestados = await ctx.db.query('atestados').collect(); const licencas = await ctx.db.query('licencas').collect(); const atestadosFiltrados = atestados.filter((a) => { const inicio = new Date(a.dataInicio); const fim = new Date(a.dataFim); return ( (inicio >= dataInicioObj && inicio <= dataFimObj) || (fim >= dataInicioObj && fim <= dataFimObj) || (inicio <= dataInicioObj && fim >= dataFimObj) ); }); const licencasFiltradas = licencas.filter((l) => { const inicio = new Date(l.dataInicio); const fim = new Date(l.dataFim); return ( (inicio >= dataInicioObj && inicio <= dataFimObj) || (fim >= dataInicioObj && fim <= dataFimObj) || (inicio <= dataInicioObj && fim >= dataFimObj) ); }); return { atestados: atestadosFiltrados, licencas: licencasFiltradas }; } }); /** * Obter dados para gráficos */ export const obterDadosGraficos = query({ args: { periodo: v.optional(v.number()) // dias (padrão: 30) }, handler: async (ctx, args) => { try { const dias = args.periodo || 30; const dataLimite = Date.now() - dias * 24 * 60 * 60 * 1000; const [atestados, licencas] = await Promise.all([ ctx.db.query('atestados').collect(), ctx.db.query('licencas').collect() ]); // Filtrar por período const atestadosFiltrados = atestados.filter( (a) => new Date(a.criadoEm) >= new Date(dataLimite) ); const licencasFiltradas = licencas.filter( (l) => new Date(l.criadoEm) >= new Date(dataLimite) ); // 1. Total de dias por tipo (para gráfico de barras) const totalDiasPorTipo: Record = { atestado_medico: 0, declaracao_comparecimento: 0, maternidade: 0, paternidade: 0, ferias: 0 }; atestadosFiltrados.forEach((a) => { const dias = calcularDias(a.dataInicio, a.dataFim); if (a.tipo === 'atestado_medico') { totalDiasPorTipo.atestado_medico += dias; } else { totalDiasPorTipo.declaracao_comparecimento += dias; } }); licencasFiltradas.forEach((l) => { const dias = calcularDias(l.dataInicio, l.dataFim); if (l.tipo === 'maternidade') { totalDiasPorTipo.maternidade += dias; } else { totalDiasPorTipo.paternidade += dias; } }); // Buscar férias do período try { const ferias = await ctx.db .query('ferias') .filter((q) => q.or( q.eq(q.field('status'), 'aprovado'), q.eq(q.field('status'), 'data_ajustada_aprovada'), q.eq(q.field('status'), 'EmFérias') ) ) .collect(); ferias.forEach((f) => { const dias = calcularDias(f.dataInicio, f.dataFim); totalDiasPorTipo.ferias += dias; }); } catch (error) { console.error('Erro ao buscar férias para gráfico:', error); } // 2. Tendências mensais (últimos 6 meses) const meses: Record< string, { atestado_medico: number; declaracao_comparecimento: number; maternidade: number; paternidade: number; ferias: number; } > = {}; const hoje = new Date(); for (let i = 5; i >= 0; i--) { const mesData = new Date(hoje.getFullYear(), hoje.getMonth() - i, 1); const mesKey = mesData.toLocaleDateString('pt-BR', { month: 'short', year: 'numeric' }); meses[mesKey] = { atestado_medico: 0, declaracao_comparecimento: 0, maternidade: 0, paternidade: 0, ferias: 0 }; } // Processar atestados para tendências mensais (usar todos, não apenas filtrados) atestados.forEach((item) => { try { const mesData = new Date(item.criadoEm); const mesKey = mesData.toLocaleDateString('pt-BR', { month: 'short', year: 'numeric' }); if (meses[mesKey]) { const dias = calcularDias(item.dataInicio, item.dataFim); if (item.tipo === 'atestado_medico') { meses[mesKey].atestado_medico += dias; } else if (item.tipo === 'declaracao_comparecimento') { meses[mesKey].declaracao_comparecimento += dias; } } } catch (error) { console.error('Erro ao processar atestado para tendências:', error); } }); // Processar licenças para tendências mensais (usar todas, não apenas filtradas) licencas.forEach((item) => { try { const mesData = new Date(item.criadoEm); const mesKey = mesData.toLocaleDateString('pt-BR', { month: 'short', year: 'numeric' }); if (meses[mesKey]) { const dias = calcularDias(item.dataInicio, item.dataFim); if (item.tipo === 'maternidade') { meses[mesKey].maternidade += dias; } else if (item.tipo === 'paternidade') { meses[mesKey].paternidade += dias; } } } catch (error) { console.error('Erro ao processar licença para tendências:', error); } }); // 3. Funcionários atualmente afastados const hojeStr = new Date().toISOString().split('T')[0]; const funcionariosAfastados: Array<{ funcionarioId: Id<'funcionarios'>; funcionarioNome: string; tipo: string; dataInicio: string; dataFim: string; }> = []; // Processar atestados (verificar funcionários atualmente afastados) atestadosFiltrados.forEach((item) => { try { const inicio = new Date(item.dataInicio); const fim = new Date(item.dataFim); const hoje = new Date(hojeStr); if (hoje >= inicio && hoje <= fim) { funcionariosAfastados.push({ funcionarioId: item.funcionarioId, funcionarioNome: 'Carregando...', tipo: item.tipo, dataInicio: item.dataInicio, dataFim: item.dataFim }); } } catch (error) { console.error('Erro ao processar atestado:', error); } }); // Processar licenças (verificar funcionários atualmente afastados) licencasFiltradas.forEach((item) => { try { const inicio = new Date(item.dataInicio); const fim = new Date(item.dataFim); const hoje = new Date(hojeStr); if (hoje >= inicio && hoje <= fim) { funcionariosAfastados.push({ funcionarioId: item.funcionarioId, funcionarioNome: 'Carregando...', tipo: item.tipo, dataInicio: item.dataInicio, dataFim: item.dataFim }); } } catch (error) { console.error('Erro ao processar licença:', error); } }); // Buscar nomes dos funcionários const funcionariosAfastadosComNomes = await Promise.all( funcionariosAfastados.map(async (item) => { try { const funcionario = await ctx.db.get(item.funcionarioId); return { ...item, funcionarioNome: funcionario?.nome || 'Desconhecido' }; } catch (error) { console.error('Erro ao buscar funcionário:', error); return { ...item, funcionarioNome: 'Desconhecido' }; } }) ); return { totalDiasPorTipo: [ { tipo: 'Atestado Médico', dias: totalDiasPorTipo.atestado_medico }, { tipo: 'Declaração', dias: totalDiasPorTipo.declaracao_comparecimento }, { tipo: 'Licença Maternidade', dias: totalDiasPorTipo.maternidade }, { tipo: 'Licença Paternidade', dias: totalDiasPorTipo.paternidade }, { tipo: 'Férias', dias: totalDiasPorTipo.ferias } ], tendenciasMensais: Object.entries(meses).map(([mes, dados]) => ({ mes, ...dados })), funcionariosAfastados: funcionariosAfastadosComNomes }; } catch (error) { console.error('Erro em obterDadosGraficos:', error); // Retornar dados vazios em caso de erro para não quebrar a página return { totalDiasPorTipo: [ { tipo: 'Atestado Médico', dias: 0 }, { tipo: 'Declaração', dias: 0 }, { tipo: 'Licença Maternidade', dias: 0 }, { tipo: 'Licença Paternidade', dias: 0 }, { tipo: 'Férias', dias: 0 } ], tendenciasMensais: [], funcionariosAfastados: [] }; } } }); /** * Obter estatísticas para dashboard */ export const obterEstatisticas = query({ args: {}, handler: async (ctx) => { const hoje = new Date(); hoje.setHours(0, 0, 0, 0); const inicioMes = new Date(hoje.getFullYear(), hoje.getMonth(), 1); const fimMes = new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0); const [atestados, licencas] = await Promise.all([ ctx.db.query('atestados').collect(), ctx.db.query('licencas').collect() ]); // Atestados ativos const atestadosAtivos = atestados.filter((a) => new Date(a.dataFim) >= hoje); // Licenças ativas const licencasAtivas = licencas.filter((l) => new Date(l.dataFim) >= hoje); // Funcionários afastados hoje const funcionariosAfastadosHoje = new Set(); [...atestados, ...licencas].forEach((item) => { const inicio = new Date(item.dataInicio); const fim = new Date(item.dataFim); if (hoje >= inicio && hoje <= fim) { funcionariosAfastadosHoje.add(item.funcionarioId); } }); // Total de dias no mês let totalDiasMes = 0; [...atestados, ...licencas].forEach((item) => { const inicio = new Date(item.dataInicio); const fim = new Date(item.dataFim); if ( (inicio >= inicioMes && inicio <= fimMes) || (fim >= inicioMes && fim <= fimMes) || (inicio <= inicioMes && fim >= fimMes) ) { const dias = calcularDias(item.dataInicio, item.dataFim); totalDiasMes += dias; } }); return { totalAtestadosAtivos: atestadosAtivos.length, totalLicencasAtivas: licencasAtivas.length, funcionariosAfastadosHoje: funcionariosAfastadosHoje.size, totalDiasAfastamentoMes: totalDiasMes }; } }); /** * Obter eventos formatados para calendário */ export const obterEventosCalendario = query({ args: { dataInicio: v.optional(v.string()), dataFim: v.optional(v.string()), tipoFiltro: v.optional( v.union( v.literal('todos'), v.literal('atestado_medico'), v.literal('declaracao_comparecimento'), v.literal('maternidade'), v.literal('paternidade'), v.literal('ferias') ) ) }, handler: async (ctx, args) => { const eventos: Array<{ id: string; title: string; start: string; end: string; color: string; tipo: string; funcionarioNome: string; funcionarioId: string; }> = []; try { // Buscar atestados if ( !args.tipoFiltro || args.tipoFiltro === 'todos' || args.tipoFiltro === 'atestado_medico' || args.tipoFiltro === 'declaracao_comparecimento' ) { try { const atestados = await ctx.db.query('atestados').collect(); for (const atestado of atestados) { try { if ( args.tipoFiltro && args.tipoFiltro !== 'todos' && atestado.tipo !== args.tipoFiltro ) { continue; } const funcionario = await ctx.db.get(atestado.funcionarioId); if (!funcionario) continue; if (!atestado.dataInicio || !atestado.dataFim) continue; const cor = atestado.tipo === 'atestado_medico' ? '#ef4444' : '#f97316'; // vermelho ou laranja eventos.push({ id: `atestado-${atestado._id}`, title: `${funcionario.nome} - ${ atestado.tipo === 'atestado_medico' ? 'Atestado Médico' : 'Declaração' }`, start: atestado.dataInicio, end: atestado.dataFim, color: cor, tipo: atestado.tipo, funcionarioNome: funcionario.nome, funcionarioId: funcionario._id }); } catch (error) { console.error(`Erro ao processar atestado ${atestado._id}:`, error); continue; } } } catch (error) { console.error('Erro ao buscar atestados:', error); } } // Buscar licenças if ( !args.tipoFiltro || args.tipoFiltro === 'todos' || args.tipoFiltro === 'maternidade' || args.tipoFiltro === 'paternidade' ) { try { const licencas = await ctx.db.query('licencas').collect(); for (const licenca of licencas) { try { if ( args.tipoFiltro && args.tipoFiltro !== 'todos' && licenca.tipo !== args.tipoFiltro ) { continue; } const funcionario = await ctx.db.get(licenca.funcionarioId); if (!funcionario) continue; if (!licenca.dataInicio || !licenca.dataFim) continue; const cor = licenca.tipo === 'maternidade' ? '#ec4899' : '#3b82f6'; // rosa ou azul eventos.push({ id: `licenca-${licenca._id}`, title: `${funcionario.nome} - Licença ${ licenca.tipo === 'maternidade' ? 'Maternidade' : 'Paternidade' }`, start: licenca.dataInicio, end: licenca.dataFim, color: cor, tipo: licenca.tipo, funcionarioNome: funcionario.nome, funcionarioId: funcionario._id }); } catch (error) { console.error(`Erro ao processar licença ${licenca._id}:`, error); continue; } } } catch (error) { console.error('Erro ao buscar licenças:', error); } } } catch (error) { console.error('Erro geral em obterEventosCalendario:', error); return eventos; // Retorna eventos já coletados mesmo se houver erro } // Integrar com férias (se não estiver filtrando por tipo específico) if (!args.tipoFiltro || args.tipoFiltro === 'todos' || args.tipoFiltro === 'ferias') { try { // Buscar férias aprovadas const ferias = await ctx.db .query('ferias') .filter((q) => q.or( q.eq(q.field('status'), 'aprovado'), q.eq(q.field('status'), 'data_ajustada_aprovada'), q.eq(q.field('status'), 'EmFérias') ) ) .collect(); for (const feriasRegistro of ferias) { try { const funcionario = await ctx.db.get(feriasRegistro.funcionarioId); if (!funcionario) continue; if (!feriasRegistro.dataInicio || !feriasRegistro.dataFim) continue; eventos.push({ id: `ferias-${feriasRegistro._id}`, title: `${funcionario.nome} - Férias`, start: feriasRegistro.dataInicio, end: feriasRegistro.dataFim, color: '#10b981', // verde tipo: 'ferias', funcionarioNome: funcionario.nome, funcionarioId: funcionario._id }); } catch (error) { console.error(`Erro ao processar férias ${feriasRegistro._id}:`, error); continue; } } } catch (error) { console.error('Erro ao buscar férias:', error); // Continua mesmo se houver erro ao buscar férias } } // Filtrar por período se fornecido if (args.dataInicio && args.dataFim) { const inicio = new Date(args.dataInicio); const fim = new Date(args.dataFim); return eventos.filter((e) => { const eventStart = new Date(e.start); const eventEnd = new Date(e.end); return ( (eventStart >= inicio && eventStart <= fim) || (eventEnd >= inicio && eventEnd <= fim) || (eventStart <= inicio && eventEnd >= fim) ); }); } return eventos; } }); // ========== MUTATIONS ========== /** * Gerar URL para upload de documentos */ export const generateUploadUrl = mutation({ args: {}, returns: v.string(), handler: async (ctx) => { const usuario = await getUsuarioAutenticado(ctx); if (!usuario) throw new Error('Não autenticado'); return await ctx.storage.generateUploadUrl(); } }); /** * Obter URL de um documento armazenado */ export const obterUrlDocumento = query({ args: { storageId: v.id('_storage') }, returns: v.union(v.string(), v.null()), handler: async (ctx, args) => { const usuario = await getUsuarioAutenticado(ctx); if (!usuario) throw new Error('Não autenticado'); return await ctx.storage.getUrl(args.storageId); } }); /** * Criar atestado médico */ export const criarAtestadoMedico = mutation({ args: { funcionarioId: v.id('funcionarios'), dataInicio: v.string(), dataFim: v.string(), cid: v.string(), observacoes: v.optional(v.string()), documentoId: v.optional(v.id('_storage')) }, returns: v.id('atestados'), handler: async (ctx, args) => { const usuario = await getUsuarioAutenticado(ctx); if (!usuario) throw new Error('Não autenticado'); // Validar datas if (new Date(args.dataFim) < new Date(args.dataInicio)) { throw new Error('Data fim deve ser maior ou igual à data início'); } const atestadoId = await ctx.db.insert('atestados', { funcionarioId: args.funcionarioId, tipo: 'atestado_medico', dataInicio: args.dataInicio, dataFim: args.dataFim, cid: args.cid, observacoes: args.observacoes, documentoId: args.documentoId, criadoPor: usuario._id, criadoEm: Date.now() }); await registrarAtividade( ctx, usuario._id, 'criar', 'atestados', `Atestado médico criado para funcionário ${args.funcionarioId}`, atestadoId ); // Recalcular banco de horas para todas as datas do período do atestado await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim); return atestadoId; } }); /** * Criar declaração de comparecimento */ export const criarDeclaracaoComparecimento = mutation({ args: { funcionarioId: v.id('funcionarios'), dataInicio: v.string(), dataFim: v.string(), observacoes: v.optional(v.string()), documentoId: v.optional(v.id('_storage')) }, returns: v.id('atestados'), handler: async (ctx, args) => { const usuario = await getUsuarioAutenticado(ctx); if (!usuario) throw new Error('Não autenticado'); // Validar datas if (new Date(args.dataFim) < new Date(args.dataInicio)) { throw new Error('Data fim deve ser maior ou igual à data início'); } const atestadoId = await ctx.db.insert('atestados', { funcionarioId: args.funcionarioId, tipo: 'declaracao_comparecimento', dataInicio: args.dataInicio, dataFim: args.dataFim, observacoes: args.observacoes, documentoId: args.documentoId, criadoPor: usuario._id, criadoEm: Date.now() }); await registrarAtividade( ctx, usuario._id, 'criar', 'atestados', `Declaração de comparecimento criada para funcionário ${args.funcionarioId}`, atestadoId ); // Recalcular banco de horas para todas as datas do período da declaração await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim); return atestadoId; } }); /** * Criar licença maternidade */ export const criarLicencaMaternidade = mutation({ args: { funcionarioId: v.id('funcionarios'), dataInicio: v.string(), dataFim: v.string(), observacoes: v.optional(v.string()), documentoId: v.optional(v.id('_storage')), licencaOriginalId: v.optional(v.id('licencas')) }, returns: v.id('licencas'), handler: async (ctx, args) => { const usuario = await getUsuarioAutenticado(ctx); if (!usuario) throw new Error('Não autenticado'); // Validar datas if (new Date(args.dataFim) < new Date(args.dataInicio)) { throw new Error('Data fim deve ser maior ou igual à data início'); } const ehProrrogacao = !!args.licencaOriginalId; if (ehProrrogacao && !args.licencaOriginalId) { throw new Error('Licença original é obrigatória para prorrogação'); } const licencaId = await ctx.db.insert('licencas', { funcionarioId: args.funcionarioId, tipo: 'maternidade', dataInicio: args.dataInicio, dataFim: args.dataFim, observacoes: args.observacoes, documentoId: args.documentoId, licencaOriginalId: args.licencaOriginalId, ehProrrogacao, criadoPor: usuario._id, criadoEm: Date.now() }); await registrarAtividade( ctx, usuario._id, 'criar', 'licencas', `Licença maternidade criada para funcionário ${args.funcionarioId}${ehProrrogacao ? ' (prorrogação)' : ''}`, licencaId ); // Recalcular banco de horas para todas as datas do período da licença await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim); return licencaId; } }); /** * Criar licença paternidade */ export const criarLicencaPaternidade = mutation({ args: { funcionarioId: v.id('funcionarios'), dataInicio: v.string(), dataFim: v.string(), observacoes: v.optional(v.string()), documentoId: v.optional(v.id('_storage')) }, returns: v.id('licencas'), handler: async (ctx, args) => { const usuario = await getUsuarioAutenticado(ctx); if (!usuario) throw new Error('Não autenticado'); // Validar datas if (new Date(args.dataFim) < new Date(args.dataInicio)) { throw new Error('Data fim deve ser maior ou igual à data início'); } const licencaId = await ctx.db.insert('licencas', { funcionarioId: args.funcionarioId, tipo: 'paternidade', dataInicio: args.dataInicio, dataFim: args.dataFim, observacoes: args.observacoes, documentoId: args.documentoId, ehProrrogacao: false, criadoPor: usuario._id, criadoEm: Date.now() }); await registrarAtividade( ctx, usuario._id, 'criar', 'licencas', `Licença paternidade criada para funcionário ${args.funcionarioId}`, licencaId ); // Recalcular banco de horas para todas as datas do período da licença await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim); return licencaId; } }); /** * Prorrogar licença maternidade */ export const prorrogarLicencaMaternidade = mutation({ args: { licencaOriginalId: v.id('licencas'), dataInicio: v.string(), dataFim: v.string(), observacoes: v.optional(v.string()), documentoId: v.optional(v.id('_storage')) }, returns: v.id('licencas'), handler: async (ctx, args) => { const usuario = await getUsuarioAutenticado(ctx); if (!usuario) throw new Error('Não autenticado'); const licencaOriginal = await ctx.db.get(args.licencaOriginalId); if (!licencaOriginal) { throw new Error('Licença original não encontrada'); } if (licencaOriginal.tipo !== 'maternidade') { throw new Error('Apenas licenças de maternidade podem ser prorrogadas'); } // Validar datas if (new Date(args.dataFim) < new Date(args.dataInicio)) { throw new Error('Data fim deve ser maior ou igual à data início'); } const prorrogacaoId = await ctx.db.insert('licencas', { funcionarioId: licencaOriginal.funcionarioId, tipo: 'maternidade', dataInicio: args.dataInicio, dataFim: args.dataFim, observacoes: args.observacoes, documentoId: args.documentoId, licencaOriginalId: args.licencaOriginalId, ehProrrogacao: true, criadoPor: usuario._id, criadoEm: Date.now() }); await registrarAtividade( ctx, usuario._id, 'criar', 'licencas', `Prorrogação de licença maternidade criada para funcionário ${licencaOriginal.funcionarioId}`, prorrogacaoId ); return prorrogacaoId; } }); /** * Excluir atestado */ export const excluirAtestado = mutation({ args: { id: v.id('atestados') }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getUsuarioAutenticado(ctx); if (!usuario) throw new Error('Não autenticado'); const atestado = await ctx.db.get(args.id); if (!atestado) throw new Error('Atestado não encontrado'); await ctx.db.delete(args.id); await registrarAtividade( ctx, usuario._id, 'excluir', 'atestados', `Atestado excluído: ${args.id}`, args.id ); return null; } }); /** * Excluir licença */ export const excluirLicenca = mutation({ args: { id: v.id('licencas') }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getUsuarioAutenticado(ctx); if (!usuario) throw new Error('Não autenticado'); const licenca = await ctx.db.get(args.id); if (!licenca) throw new Error('Licença não encontrada'); await ctx.db.delete(args.id); await registrarAtividade( ctx, usuario._id, 'excluir', 'licencas', `Licença excluída: ${args.id}`, args.id ); return null; } });