- Implemented an internal mutation for login that captures the user's IP address and user agent for better security and tracking. - Enhanced the HTTP login endpoint to extract and log client IP, improving the overall authentication process. - Added validation for IP addresses to ensure only valid formats are recorded, enhancing data integrity. - Updated the login mutation to handle rate limiting and user status checks more effectively, providing clearer feedback on login attempts.
1049 lines
30 KiB
TypeScript
1049 lines
30 KiB
TypeScript
import { v } from "convex/values";
|
|
import { mutation, query } from "./_generated/server";
|
|
import { Id, Doc } from "./_generated/dataModel";
|
|
import type { QueryCtx, MutationCtx } from "./_generated/server";
|
|
import { registrarAtividade } from "./logsAtividades";
|
|
|
|
// ========== HELPERS ==========
|
|
|
|
/**
|
|
* Helper function para obter usuário autenticado
|
|
*/
|
|
async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) {
|
|
const identity = await ctx.auth.getUserIdentity();
|
|
let usuarioAtual = null;
|
|
|
|
if (identity && identity.email) {
|
|
usuarioAtual = await ctx.db
|
|
.query("usuarios")
|
|
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
|
.first();
|
|
}
|
|
|
|
if (!usuarioAtual) {
|
|
const sessaoAtiva = await ctx.db
|
|
.query("sessoes")
|
|
.filter((q) => q.eq(q.field("ativo"), true))
|
|
.order("desc")
|
|
.first();
|
|
|
|
if (sessaoAtiva) {
|
|
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
|
|
}
|
|
}
|
|
|
|
return usuarioAtual;
|
|
}
|
|
|
|
/**
|
|
* 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 solicitacoesFerias = await ctx.db
|
|
.query("solicitacoesFerias")
|
|
.filter((q) =>
|
|
q.or(
|
|
q.eq(q.field("status"), "aprovado"),
|
|
q.eq(q.field("status"), "data_ajustada_aprovada")
|
|
)
|
|
)
|
|
.collect();
|
|
|
|
solicitacoesFerias.forEach((s) => {
|
|
if (s.periodos && Array.isArray(s.periodos)) {
|
|
s.periodos.forEach((p: { dataInicio: string; dataFim: string }) => {
|
|
const dias = calcularDias(p.dataInicio, p.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 solicitações de férias aprovadas
|
|
const solicitacoesFerias = await ctx.db
|
|
.query("solicitacoesFerias")
|
|
.filter((q) =>
|
|
q.or(
|
|
q.eq(q.field("status"), "aprovado"),
|
|
q.eq(q.field("status"), "data_ajustada_aprovada")
|
|
)
|
|
)
|
|
.collect();
|
|
|
|
for (const solicitacao of solicitacoesFerias) {
|
|
try {
|
|
const funcionario = await ctx.db.get(solicitacao.funcionarioId);
|
|
if (!funcionario) continue;
|
|
|
|
// Verificar se periodos existe e é um array
|
|
if (!solicitacao.periodos || !Array.isArray(solicitacao.periodos)) {
|
|
continue;
|
|
}
|
|
|
|
for (const periodo of solicitacao.periodos) {
|
|
if (!periodo.dataInicio || !periodo.dataFim) continue;
|
|
|
|
eventos.push({
|
|
id: `ferias-${solicitacao._id}-${periodo.dataInicio}`,
|
|
title: `${funcionario.nome} - Férias`,
|
|
start: periodo.dataInicio,
|
|
end: periodo.dataFim,
|
|
color: "#10b981", // verde
|
|
tipo: "ferias",
|
|
funcionarioNome: funcionario.nome,
|
|
funcionarioId: funcionario._id,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error(`Erro ao processar solicitação de férias ${solicitacao._id}:`, error);
|
|
continue;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao buscar solicitações de 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();
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
});
|