Merge remote-tracking branch 'origin' into feat-pedidos
This commit is contained in:
@@ -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()
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user