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

@@ -1,8 +1,11 @@
import { v } from 'convex/values';
import { api, internal } from './_generated/api';
import type { Doc, Id } from './_generated/dataModel';
import type { MutationCtx, QueryCtx } from './_generated/server';
import { mutation, query } from './_generated/server';
import type { QueryCtx, MutationCtx } from './_generated/server';
import { api } from './_generated/api';
import { internal } from './_generated/api';
import { Id, Doc } from './_generated/dataModel';
import { parseLocalDate, formatarDataBR } from './utils/datas';
import { getCurrentUserFunction } from './auth';
// Query: Listar todas as solicitações (para RH)
export const listarTodas = query({
@@ -26,9 +29,26 @@ export const listarTodas = query({
time = await ctx.db.get(membroTime.timeId);
}
// Buscar usuário do funcionário para obter fotoPerfilUrl
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 {
...s,
funcionario,
funcionario: funcionario
? {
...funcionario,
fotoPerfilUrl
}
: null,
time
};
})
@@ -40,7 +60,10 @@ export const listarTodas = query({
// Query: Listar solicitações do funcionário
export const listarMinhasSolicitacoes = query({
args: { funcionarioId: v.id('funcionarios') },
args: {
funcionarioId: v.id('funcionarios'),
_refresh: v.optional(v.number()) // Parâmetro para forçar atualização no frontend
},
handler: async (ctx, args) => {
const solicitacoes = await ctx.db
.query('solicitacoesAusencias')
@@ -90,7 +113,7 @@ export const listarSolicitacoesSubordinados = query({
const solicitacoes: Array<
Doc<'solicitacoesAusencias'> & {
funcionario: Doc<'funcionarios'> | null;
funcionario: (Doc<'funcionarios'> & { fotoPerfilUrl: string | null }) | null;
time: Doc<'times'> | null;
}
> = [];
@@ -112,9 +135,27 @@ export const listarSolicitacoesSubordinados = query({
// Adicionar info do funcionário
for (const s of solic) {
const funcionario = await ctx.db.get(s.funcionarioId);
// Buscar usuário do funcionário para obter fotoPerfilUrl
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);
}
}
solicitacoes.push({
...s,
funcionario,
funcionario: funcionario
? {
...funcionario,
fotoPerfilUrl
}
: null,
time
});
}
@@ -133,6 +174,19 @@ export const obterDetalhes = query({
if (!solicitacao) return null;
const funcionario = await ctx.db.get(solicitacao.funcionarioId);
// Buscar usuário do funcionário para obter fotoPerfilUrl
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);
}
}
let gestor = null;
if (solicitacao.gestorId) {
gestor = await ctx.db.get(solicitacao.gestorId);
@@ -150,11 +204,31 @@ export const obterDetalhes = query({
time = await ctx.db.get(membroTime.timeId);
}
// Enriquecer histórico com nomes dos usuários
let historicoComUsuarios = solicitacao.historicoAlteracoes;
if (historicoComUsuarios && historicoComUsuarios.length > 0) {
historicoComUsuarios = await Promise.all(
historicoComUsuarios.map(async (hist) => {
const usuario = await ctx.db.get(hist.usuarioId);
return {
...hist,
usuarioNome: usuario?.nome || 'Usuário Desconhecido'
};
})
);
}
return {
...solicitacao,
funcionario,
funcionario: funcionario
? {
...funcionario,
fotoPerfilUrl
}
: null,
gestor,
time
time,
historicoAlteracoes: historicoComUsuarios
};
}
});
@@ -211,6 +285,36 @@ export const contarPendentesGestor = query({
}
});
// Helper: 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: Verificar se há sobreposição de datas
function verificarSobreposicao(
inicio1: string,
@@ -218,10 +322,10 @@ function verificarSobreposicao(
inicio2: string,
fim2: string
): boolean {
const d1Inicio = new Date(inicio1);
const d1Fim = new Date(fim1);
const d2Inicio = new Date(inicio2);
const d2Fim = new Date(fim2);
const d1Inicio = parseLocalDate(inicio1);
const d1Fim = parseLocalDate(fim1);
const d2Inicio = parseLocalDate(inicio2);
const d2Fim = parseLocalDate(fim2);
return d1Inicio <= d2Fim && d2Inicio <= d1Fim;
}
@@ -260,12 +364,16 @@ export const criarSolicitacao = mutation({
throw new Error('O motivo deve ter no mínimo 10 caracteres');
}
const dataInicio = new Date(args.dataInicio);
const dataFim = new Date(args.dataFim);
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
const dataInicio = parseLocalDate(args.dataInicio);
const dataFim = parseLocalDate(args.dataFim);
if (dataInicio < hoje) {
// Criar data de hoje em UTC para comparação
const hoje = new Date();
const hojeUTC = new Date(
Date.UTC(hoje.getUTCFullYear(), hoje.getUTCMonth(), hoje.getUTCDate(), 0, 0, 0, 0)
);
if (dataInicio < hojeUTC) {
throw new Error('A data de início não pode ser no passado');
}
@@ -278,6 +386,17 @@ export const criarSolicitacao = mutation({
throw new Error('Funcionário não encontrado');
}
// Buscar usuário que está criando (pode não ser o próprio funcionário)
const usuarioCriador = await getCurrentUserFunction(ctx);
const usuarioFuncionario = await ctx.db
.query('usuarios')
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', args.funcionarioId))
.first();
// Usar o usuário autenticado se disponível, senão usar o usuário do funcionário ou gestor
const usuarioIdParaHistorico =
usuarioCriador?._id || usuarioFuncionario?._id || funcionario.gestorId;
// Verificar sobreposição com outras solicitações aprovadas ou pendentes
const solicitacoesExistentes = await ctx.db
.query('solicitacoesAusencias')
@@ -292,6 +411,17 @@ export const criarSolicitacao = mutation({
}
}
// Criar histórico inicial
const historicoInicial = usuarioIdParaHistorico
? [
{
data: Date.now(),
usuarioId: usuarioIdParaHistorico,
acao: 'Solicitação criada'
}
]
: [];
// Criar solicitação
const solicitacaoId = await ctx.db.insert('solicitacoesAusencias', {
funcionarioId: args.funcionarioId,
@@ -299,7 +429,8 @@ export const criarSolicitacao = mutation({
dataFim: args.dataFim,
motivo: args.motivo.trim(),
status: 'aguardando_aprovacao',
criadoEm: Date.now()
criadoEm: Date.now(),
historicoAlteracoes: historicoInicial
});
// Encontrar gestor do funcionário
@@ -312,7 +443,7 @@ export const criarSolicitacao = mutation({
solicitacaoAusenciaId: solicitacaoId,
tipo: 'nova_solicitacao',
lida: false,
mensagem: `${funcionario.nome} solicitou uma ausência de ${new Date(args.dataInicio).toLocaleDateString('pt-BR')} até ${new Date(args.dataFim).toLocaleDateString('pt-BR')}`
mensagem: `${funcionario.nome} solicitou uma ausência de ${formatarDataBR(args.dataInicio)} até ${formatarDataBR(args.dataFim)}`
});
// Buscar usuário do gestor para enviar email e chat
@@ -338,8 +469,8 @@ export const criarSolicitacao = mutation({
variaveis: {
gestorNome: gestorUsuario.nome,
funcionarioNome: funcionario.nome,
dataInicio: new Date(args.dataInicio).toLocaleDateString('pt-BR'),
dataFim: new Date(args.dataFim).toLocaleDateString('pt-BR'),
dataInicio: formatarDataBR(args.dataInicio),
dataFim: formatarDataBR(args.dataFim),
motivo: args.motivo,
urlSistema
},
@@ -358,7 +489,7 @@ export const criarSolicitacao = mutation({
corpo: `<p>Olá ${gestorUsuario.nome},</p>
<p>O funcionário <strong>${funcionario.nome}</strong> solicitou uma ausência:</p>
<ul>
<li><strong>Período:</strong> ${new Date(args.dataInicio).toLocaleDateString('pt-BR')} até ${new Date(args.dataFim).toLocaleDateString('pt-BR')}</li>
<li><strong>Período:</strong> ${formatarDataBR(args.dataInicio)} até ${formatarDataBR(args.dataFim)}</li>
<li><strong>Motivo:</strong> ${args.motivo}</li>
</ul>
<p>Por favor, acesse o sistema para aprovar ou reprovar esta solicitação.</p>`,
@@ -398,7 +529,7 @@ export const criarSolicitacao = mutation({
conversaId,
remetenteId: funcionarioUsuario._id,
tipo: 'texto',
conteudo: `Solicitei uma ausência de ${new Date(args.dataInicio).toLocaleDateString('pt-BR')} até ${new Date(args.dataFim).toLocaleDateString('pt-BR')}. Motivo: ${args.motivo}`,
conteudo: `Solicitei uma ausência de ${formatarDataBR(args.dataInicio)} até ${formatarDataBR(args.dataFim)}. Motivo: ${args.motivo}`,
enviadaEm: Date.now()
});
}
@@ -440,11 +571,23 @@ export const aprovar = mutation({
throw new Error('Funcionário não encontrado');
}
// Buscar nome do gestor para o histórico
const gestorUsuario = await ctx.db.get(args.gestorId);
const nomeGestor = gestorUsuario?.nome || 'Gestor';
// Atualizar solicitação
await ctx.db.patch(args.solicitacaoId, {
status: 'aprovado',
gestorId: args.gestorId,
dataAprovacao: Date.now()
dataAprovacao: Date.now(),
historicoAlteracoes: [
...(solicitacao.historicoAlteracoes || []),
{
data: Date.now(),
usuarioId: args.gestorId,
acao: `Aprovado por ${nomeGestor}`
}
]
});
// Buscar usuário do funcionário
@@ -460,7 +603,7 @@ export const aprovar = mutation({
solicitacaoAusenciaId: args.solicitacaoId,
tipo: 'aprovado',
lida: false,
mensagem: `Sua solicitação de ausência de ${new Date(solicitacao.dataInicio).toLocaleDateString('pt-BR')} até ${new Date(solicitacao.dataFim).toLocaleDateString('pt-BR')} foi aprovada!`
mensagem: `Sua solicitação de ausência de ${formatarDataBR(solicitacao.dataInicio)} até ${formatarDataBR(solicitacao.dataFim)} foi aprovada!`
});
const gestorUsuario = await ctx.db.get(args.gestorId);
@@ -481,8 +624,8 @@ export const aprovar = mutation({
variaveis: {
funcionarioNome: funcionarioUsuario.nome,
gestorNome: gestorUsuario.nome,
dataInicio: new Date(solicitacao.dataInicio).toLocaleDateString('pt-BR'),
dataFim: new Date(solicitacao.dataFim).toLocaleDateString('pt-BR'),
dataInicio: formatarDataBR(solicitacao.dataInicio),
dataFim: formatarDataBR(solicitacao.dataFim),
motivo: solicitacao.motivo,
urlSistema
},
@@ -501,7 +644,7 @@ export const aprovar = mutation({
corpo: `<p>Olá ${funcionarioUsuario.nome},</p>
<p>Sua solicitação de ausência foi <strong>aprovada</strong> pelo gestor ${gestorUsuario.nome}:</p>
<ul>
<li><strong>Período:</strong> ${new Date(solicitacao.dataInicio).toLocaleDateString('pt-BR')} até ${new Date(solicitacao.dataFim).toLocaleDateString('pt-BR')}</li>
<li><strong>Período:</strong> ${formatarDataBR(solicitacao.dataInicio)} até ${formatarDataBR(solicitacao.dataFim)}</li>
<li><strong>Motivo:</strong> ${solicitacao.motivo}</li>
</ul>`,
enviadoPor: args.gestorId
@@ -540,12 +683,20 @@ export const aprovar = mutation({
conversaId,
remetenteId: args.gestorId,
tipo: 'texto',
conteudo: `Aprovei sua solicitação de ausência de ${new Date(solicitacao.dataInicio).toLocaleDateString('pt-BR')} até ${new Date(solicitacao.dataFim).toLocaleDateString('pt-BR')}.`,
conteudo: `Aprovei sua solicitação de ausência de ${formatarDataBR(solicitacao.dataInicio)} até ${formatarDataBR(solicitacao.dataFim)}.`,
enviadaEm: Date.now()
});
}
}
// Recalcular banco de horas para todas as datas do período da ausência aprovada
await recalcularBancoHorasPeriodo(
ctx,
solicitacao.funcionarioId,
solicitacao.dataInicio,
solicitacao.dataFim
);
return null;
}
});
@@ -583,12 +734,24 @@ export const reprovar = mutation({
throw new Error('Funcionário não encontrado');
}
// Buscar nome do gestor para o histórico
const gestorUsuario = await ctx.db.get(args.gestorId);
const nomeGestor = gestorUsuario?.nome || 'Gestor';
// Atualizar solicitação
await ctx.db.patch(args.solicitacaoId, {
status: 'reprovado',
gestorId: args.gestorId,
dataReprovacao: Date.now(),
motivoReprovacao: args.motivoReprovacao.trim()
motivoReprovacao: args.motivoReprovacao.trim(),
historicoAlteracoes: [
...(solicitacao.historicoAlteracoes || []),
{
data: Date.now(),
usuarioId: args.gestorId,
acao: `Reprovado por ${nomeGestor}: ${args.motivoReprovacao.trim()}`
}
]
});
// Buscar usuário do funcionário
@@ -604,7 +767,7 @@ export const reprovar = mutation({
solicitacaoAusenciaId: args.solicitacaoId,
tipo: 'reprovado',
lida: false,
mensagem: `Sua solicitação de ausência de ${new Date(solicitacao.dataInicio).toLocaleDateString('pt-BR')} até ${new Date(solicitacao.dataFim).toLocaleDateString('pt-BR')} foi reprovada. Motivo: ${args.motivoReprovacao}`
mensagem: `Sua solicitação de ausência de ${formatarDataBR(solicitacao.dataInicio)} até ${formatarDataBR(solicitacao.dataFim)} foi reprovada. Motivo: ${args.motivoReprovacao}`
});
const gestorUsuario = await ctx.db.get(args.gestorId);
@@ -625,8 +788,8 @@ export const reprovar = mutation({
variaveis: {
funcionarioNome: funcionarioUsuario.nome,
gestorNome: gestorUsuario.nome,
dataInicio: new Date(solicitacao.dataInicio).toLocaleDateString('pt-BR'),
dataFim: new Date(solicitacao.dataFim).toLocaleDateString('pt-BR'),
dataInicio: formatarDataBR(solicitacao.dataInicio),
dataFim: formatarDataBR(solicitacao.dataFim),
motivo: solicitacao.motivo,
motivoReprovacao: args.motivoReprovacao,
urlSistema
@@ -646,7 +809,7 @@ export const reprovar = mutation({
corpo: `<p>Olá ${funcionarioUsuario.nome},</p>
<p>Sua solicitação de ausência foi <strong>reprovada</strong> pelo gestor ${gestorUsuario.nome}:</p>
<ul>
<li><strong>Período:</strong> ${new Date(solicitacao.dataInicio).toLocaleDateString('pt-BR')} até ${new Date(solicitacao.dataFim).toLocaleDateString('pt-BR')}</li>
<li><strong>Período:</strong> ${formatarDataBR(solicitacao.dataInicio)} até ${formatarDataBR(solicitacao.dataFim)}</li>
<li><strong>Motivo:</strong> ${solicitacao.motivo}</li>
<li><strong>Motivo da Reprovação:</strong> ${args.motivoReprovacao}</li>
</ul>`,
@@ -686,7 +849,7 @@ export const reprovar = mutation({
conversaId,
remetenteId: args.gestorId,
tipo: 'texto',
conteudo: `Reprovei sua solicitação de ausência de ${new Date(solicitacao.dataInicio).toLocaleDateString('pt-BR')} até ${new Date(solicitacao.dataFim).toLocaleDateString('pt-BR')}. Motivo: ${args.motivoReprovacao}`,
conteudo: `Reprovei sua solicitação de ausência de ${formatarDataBR(solicitacao.dataInicio)} até ${formatarDataBR(solicitacao.dataFim)}. Motivo: ${args.motivoReprovacao}`,
enviadaEm: Date.now()
});
}