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

1333 lines
38 KiB
TypeScript

import { v } from 'convex/values';
import type { Id } from './_generated/dataModel';
import type { MutationCtx, QueryCtx } from './_generated/server';
import { mutation, query } from './_generated/server';
import { getCurrentUserFunction } from './auth';
import { internal } from './_generated/api';
import { registrarAtividade } from './logsAtividades';
// ========== 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<void> {
// 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]!
});
}
}
/**
* Helper para verificar se um funcionário tem licença ou atestado ativo
* Retorna true se há algum registro ativo (data atual entre dataInicio e dataFim)
*/
export async function verificarLicencaAtiva(
ctx: QueryCtx | MutationCtx,
funcionarioId: Id<'funcionarios'>,
dataAtual?: Date
): Promise<boolean> {
// Normalizar data atual para comparar apenas a parte da data (sem hora)
// Usar timezone local para evitar problemas de conversão
const hoje = dataAtual || new Date();
const hojeStr = `${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}-${String(hoje.getDate()).padStart(2, '0')}`; // Formato: "YYYY-MM-DD"
console.log(
`[verificarLicencaAtiva] Verificando funcionário ${funcionarioId}, data atual: ${hojeStr}`
);
// Buscar atestados e licenças do funcionário
const [atestados, licencas] = await Promise.all([
ctx.db
.query('atestados')
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', funcionarioId))
.collect(),
ctx.db
.query('licencas')
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', funcionarioId))
.collect()
]);
console.log(
`[verificarLicencaAtiva] Encontrados ${atestados.length} atestados e ${licencas.length} licenças`
);
// Verificar se há algum atestado ativo
for (const atestado of atestados) {
// Normalizar datas para formato "YYYY-MM-DD" (pode vir como "YYYY-MM-DD" ou "YYYY-MM-DDTHH:mm:ss")
const inicioStr = atestado.dataInicio.includes('T')
? atestado.dataInicio.split('T')[0]
: atestado.dataInicio.substring(0, 10);
const fimStr = atestado.dataFim.includes('T')
? atestado.dataFim.split('T')[0]
: atestado.dataFim.substring(0, 10);
console.log(
`[verificarLicencaAtiva] Atestado: ${inicioStr} a ${fimStr}, hoje: ${hojeStr}, ativo: ${hojeStr >= inicioStr && hojeStr <= fimStr}`
);
// Comparar strings de data diretamente (formato ISO permite comparação lexicográfica)
if (hojeStr >= inicioStr && hojeStr <= fimStr) {
console.log(`[verificarLicencaAtiva] ✅ Atestado ativo encontrado!`);
return true;
}
}
// Verificar se há alguma licença ativa
for (const licenca of licencas) {
// Normalizar datas para formato "YYYY-MM-DD" (pode vir como "YYYY-MM-DD" ou "YYYY-MM-DDTHH:mm:ss")
const inicioStr = licenca.dataInicio.includes('T')
? licenca.dataInicio.split('T')[0]
: licenca.dataInicio.substring(0, 10);
const fimStr = licenca.dataFim.includes('T')
? licenca.dataFim.split('T')[0]
: licenca.dataFim.substring(0, 10);
console.log(
`[verificarLicencaAtiva] Licença: ${inicioStr} a ${fimStr}, hoje: ${hojeStr}, ativa: ${hojeStr >= inicioStr && hojeStr <= fimStr}`
);
// Comparar strings de data diretamente (formato ISO permite comparação lexicográfica)
if (hojeStr >= inicioStr && hojeStr <= fimStr) {
console.log(`[verificarLicencaAtiva] ✅ Licença ativa encontrada!`);
return true;
}
}
console.log(`[verificarLicencaAtiva] ❌ Nenhuma licença/atestado ativo encontrado`);
return false;
}
// ========== QUERIES ==========
/**
* Listar todos os atestados e licenças com detalhes do funcionário
*/
export const listarTodos = query({
args: {},
handler: async (ctx) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'listar'
});
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) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'listar'
});
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) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'listar'
});
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
};
}
});
/**
* Verificar se o funcionário atual tem licença/atestado ativo
*/
export const verificarStatusLicenca = query({
args: {
funcionarioId: v.id('funcionarios')
},
returns: v.boolean(),
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'listar'
});
return await verificarLicencaAtiva(ctx, args.funcionarioId);
}
});
/**
* Obter dados para gráficos
*/
export const obterDadosGraficos = query({
args: {
periodo: v.optional(v.number()) // dias (padrão: 30)
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'dashboard'
});
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<string, number> = {
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) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'dashboard'
});
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<string>();
[...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) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'dashboard'
});
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);
}
}
} 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);
}
}
} 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);
}
}
} 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) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'criar'
});
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) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'listar'
});
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) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'criar'
});
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);
// Atualizar status do funcionário imediatamente
console.log(
`[criarAtestadoMedico] Atualizando status do funcionário ${args.funcionarioId} após criar atestado`
);
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
funcionarioId: args.funcionarioId
});
console.log(`[criarAtestadoMedico] Status atualizado com sucesso`);
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) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'criar'
});
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);
// Atualizar status do funcionário imediatamente
console.log(
`[criarDeclaracaoComparecimento] Atualizando status do funcionário ${args.funcionarioId} após criar declaração`
);
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
funcionarioId: args.funcionarioId
});
console.log(`[criarDeclaracaoComparecimento] Status atualizado com sucesso`);
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) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'criar'
});
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);
// Atualizar status do funcionário imediatamente
console.log(
`[criarLicencaMaternidade] Atualizando status do funcionário ${args.funcionarioId} após criar licença maternidade`
);
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
funcionarioId: args.funcionarioId
});
console.log(`[criarLicencaMaternidade] Status atualizado com sucesso`);
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) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'criar'
});
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);
// Atualizar status do funcionário imediatamente
console.log(
`[criarLicencaPaternidade] Atualizando status do funcionário ${args.funcionarioId} após criar licença paternidade`
);
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
funcionarioId: args.funcionarioId
});
console.log(`[criarLicencaPaternidade] Status atualizado com sucesso`);
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) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'editar'
});
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
);
// Atualizar status do funcionário imediatamente
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
funcionarioId: licencaOriginal.funcionarioId
});
return prorrogacaoId;
}
});
/**
* Excluir atestado
*/
export const excluirAtestado = mutation({
args: {
id: v.id('atestados')
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'excluir'
});
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');
// IMPORTANTE: Salvar o período exato do atestado ANTES de excluir
// para recalcular o banco de horas apenas para esse período específico
const funcionarioId = atestado.funcionarioId;
const dataInicio = atestado.dataInicio; // Data início do atestado
const dataFim = atestado.dataFim; // Data fim do atestado
// Excluir o registro do banco de dados
await ctx.db.delete(args.id);
await registrarAtividade(
ctx,
usuario._id,
'excluir',
'atestados',
`Atestado excluído: ${args.id}`,
args.id
);
// Recalcular banco de horas APENAS para o período específico do atestado excluído
// Isso garante que os dias do atestado sejam removidos corretamente dos registros de ponto
await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim);
// Atualizar status do funcionário imediatamente
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
funcionarioId
});
return null;
}
});
/**
* Excluir licença
*/
export const excluirLicenca = mutation({
args: {
id: v.id('licencas')
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'atestados_licencas',
acao: 'excluir'
});
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');
// IMPORTANTE: Salvar o período exato da licença ANTES de excluir
// para recalcular o banco de horas apenas para esse período específico
const funcionarioId = licenca.funcionarioId;
const dataInicio = licenca.dataInicio; // Data início da licença
const dataFim = licenca.dataFim; // Data fim da licença
// Excluir o registro do banco de dados
await ctx.db.delete(args.id);
await registrarAtividade(
ctx,
usuario._id,
'excluir',
'licencas',
`Licença excluída: ${args.id}`,
args.id
);
// Recalcular banco de horas APENAS para o período específico da licença excluída
// Isso garante que os dias da licença sejam removidos corretamente dos registros de ponto
await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim);
// Atualizar status do funcionário imediatamente
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
funcionarioId
});
return null;
}
});