- Renamed and refactored vacation-related types and components for clarity, transitioning from 'SolicitacaoFerias' to 'PeriodoFerias'. - Improved the handling of vacation statuses, including the addition of 'EmFérias' to the status options. - Streamlined the vacation request and approval components to better reflect individual vacation periods. - Enhanced data handling in backend queries and schema to support the new structure and ensure accurate status updates. - Improved user experience by refining UI elements related to vacation periods and their statuses.
1016 lines
27 KiB
TypeScript
1016 lines
27 KiB
TypeScript
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';
|
|
|
|
// ========== 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;
|
|
}
|
|
|
|
// ========== 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);
|
|
return {
|
|
...a,
|
|
funcionario,
|
|
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,
|
|
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;
|
|
return {
|
|
...l,
|
|
funcionario,
|
|
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,
|
|
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<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) => {
|
|
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) => {
|
|
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
|
|
);
|
|
|
|
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
|
|
);
|
|
|
|
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
|
|
);
|
|
|
|
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
|
|
);
|
|
|
|
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;
|
|
}
|
|
});
|