Merge remote-tracking branch 'origin' into feat-pedidos

This commit is contained in:
2025-12-11 10:08:12 -03:00
194 changed files with 30374 additions and 10247 deletions

View File

@@ -3,6 +3,7 @@ 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 ==========
@@ -26,6 +27,118 @@ function calcularDias(dataInicio: string, dataFim: string): number {
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 ==========
/**
@@ -45,9 +158,23 @@ export const listarTodos = query({
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'
@@ -57,6 +184,7 @@ export const listarTodos = query({
return {
...a,
funcionario: null,
fotoPerfilUrl: null,
criadoPorNome: 'Sistema',
dias: calcularDias(a.dataInicio, a.dataFim),
status: new Date(a.dataFim) >= new Date() ? 'ativo' : 'finalizado'
@@ -73,9 +201,23 @@ export const listarTodos = query({
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),
@@ -86,6 +228,7 @@ export const listarTodos = query({
return {
...l,
funcionario: null,
fotoPerfilUrl: null,
criadoPorNome: 'Sistema',
licencaOriginal: null,
dias: calcularDias(l.dataInicio, l.dataFim),
@@ -175,6 +318,19 @@ export const listarPorPeriodo = query({
}
});
/**
* 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) => {
return await verificarLicencaAtiva(ctx, args.funcionarioId);
}
});
/**
* Obter dados para gráficos
*/
@@ -747,6 +903,18 @@ export const criarAtestadoMedico = mutation({
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;
}
});
@@ -792,6 +960,18 @@ export const criarDeclaracaoComparecimento = mutation({
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;
}
});
@@ -845,6 +1025,18 @@ export const criarLicencaMaternidade = mutation({
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;
}
});
@@ -891,6 +1083,18 @@ export const criarLicencaPaternidade = mutation({
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;
}
});
@@ -947,6 +1151,11 @@ export const prorrogarLicencaMaternidade = mutation({
prorrogacaoId
);
// Atualizar status do funcionário imediatamente
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
funcionarioId: licencaOriginal.funcionarioId
});
return prorrogacaoId;
}
});
@@ -966,6 +1175,13 @@ export const excluirAtestado = mutation({
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(
@@ -977,6 +1193,15 @@ export const excluirAtestado = mutation({
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;
}
});
@@ -996,6 +1221,13 @@ export const excluirLicenca = mutation({
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(
@@ -1007,6 +1239,15 @@ export const excluirLicenca = mutation({
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;
}
});