feat: add new features for point management and registration
- Introduced "Homologação de Registro" and "Dispensa de Registro" sections in the dashboard for enhanced point management. - Updated the WidgetGestaoPontos component to include new links and icons for the added features. - Enhanced backend functionality to support the new features, including querying and managing dispensas and homologações. - Improved the PDF generation process to include daily balance calculations for employee time records. - Implemented checks for active dispensas to prevent unauthorized point registrations.
This commit is contained in:
@@ -173,6 +173,55 @@ export const registrarPonto = mutation({
|
||||
throw new Error('Já existe um registro neste minuto');
|
||||
}
|
||||
|
||||
// Verificar se funcionário está dispensado de registrar ponto
|
||||
const dispensas = await ctx.db
|
||||
.query('dispensasRegistro')
|
||||
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', funcionarioId))
|
||||
.filter((q) => q.eq(q.field('ativo'), true))
|
||||
.collect();
|
||||
|
||||
const dataConsulta = new Date(data);
|
||||
for (const dispensa of dispensas) {
|
||||
// Se for isento, sempre está dispensado
|
||||
if (dispensa.isento) {
|
||||
throw new Error('Registro dispensado pelo gestor: Isento de registro (caso excepcional)');
|
||||
}
|
||||
|
||||
// Verificar se está no período
|
||||
const dataInicio = new Date(dispensa.dataInicio);
|
||||
const dataFim = new Date(dispensa.dataFim);
|
||||
|
||||
if (dataConsulta >= dataInicio && dataConsulta <= dataFim) {
|
||||
// Verificar hora e minuto se necessário
|
||||
const timestampConsulta = new Date(
|
||||
`${data}T${hora.toString().padStart(2, '0')}:${minuto.toString().padStart(2, '0')}:00`
|
||||
).getTime();
|
||||
const timestampInicio = new Date(
|
||||
`${dispensa.dataInicio}T${dispensa.horaInicio.toString().padStart(2, '0')}:${dispensa.minutoInicio.toString().padStart(2, '0')}:00`
|
||||
).getTime();
|
||||
const timestampFim = new Date(
|
||||
`${dispensa.dataFim}T${dispensa.horaFim.toString().padStart(2, '0')}:${dispensa.minutoFim.toString().padStart(2, '0')}:00`
|
||||
).getTime();
|
||||
|
||||
if (timestampConsulta >= timestampInicio && timestampConsulta <= timestampFim) {
|
||||
throw new Error(`Registro dispensado pelo gestor: ${dispensa.motivo}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar se expirou (desativar na mutation de registro)
|
||||
const agora = new Date();
|
||||
const dataFimTimestamp = new Date(
|
||||
`${dispensa.dataFim}T${dispensa.horaFim.toString().padStart(2, '0')}:${dispensa.minutoFim.toString().padStart(2, '0')}:00`
|
||||
).getTime();
|
||||
|
||||
if (agora.getTime() > dataFimTimestamp && !dispensa.isento) {
|
||||
// Desativar dispensa expirada (mutation pode fazer isso)
|
||||
await ctx.db.patch(dispensa._id, {
|
||||
ativo: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Determinar tipo de registro
|
||||
const tipo = await determinarTipoRegistro(ctx, usuario.funcionarioId, data);
|
||||
|
||||
@@ -274,6 +323,45 @@ export const listarRegistrosDia = query({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obtém saldo diário de um funcionário para uma data específica
|
||||
*/
|
||||
export const obterSaldoDiario = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
data: v.string(), // YYYY-MM-DD
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar banco de horas do dia
|
||||
const bancoHoras = await ctx.db
|
||||
.query('bancoHoras')
|
||||
.withIndex('by_funcionario_data', (q) =>
|
||||
q.eq('funcionarioId', args.funcionarioId).eq('data', args.data)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (!bancoHoras) {
|
||||
return {
|
||||
saldoMinutos: 0,
|
||||
horas: 0,
|
||||
minutos: 0,
|
||||
positivo: true,
|
||||
};
|
||||
}
|
||||
|
||||
const horas = Math.floor(Math.abs(bancoHoras.saldoMinutos) / 60);
|
||||
const minutos = Math.abs(bancoHoras.saldoMinutos) % 60;
|
||||
const positivo = bancoHoras.saldoMinutos >= 0;
|
||||
|
||||
return {
|
||||
saldoMinutos: bancoHoras.saldoMinutos,
|
||||
horas,
|
||||
minutos,
|
||||
positivo,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Lista registros por período (para RH)
|
||||
*/
|
||||
@@ -313,8 +401,32 @@ export const listarRegistrosPeriodo = query({
|
||||
Array.from(funcionariosIds).map((id) => ctx.db.get(id))
|
||||
);
|
||||
|
||||
// Buscar saldos diários para cada data/funcionário
|
||||
const saldosPorDataFuncionario: Record<string, number> = {};
|
||||
const datasUnicas = new Set(registrosFiltrados.map((r) => `${r.funcionarioId}-${r.data}`));
|
||||
|
||||
for (const chave of datasUnicas) {
|
||||
const [funcId, data] = chave.split('-');
|
||||
const bancoHoras = await ctx.db
|
||||
.query('bancoHoras')
|
||||
.withIndex('by_funcionario_data', (q) =>
|
||||
q.eq('funcionarioId', funcId as Id<'funcionarios'>).eq('data', data)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (bancoHoras) {
|
||||
saldosPorDataFuncionario[chave] = bancoHoras.saldoMinutos;
|
||||
}
|
||||
}
|
||||
|
||||
return registrosFiltrados.map((registro) => {
|
||||
const funcionario = funcionarios.find((f) => f?._id === registro.funcionarioId);
|
||||
const chave = `${registro.funcionarioId}-${registro.data}`;
|
||||
const saldoMinutos = saldosPorDataFuncionario[chave] || 0;
|
||||
const horas = Math.floor(Math.abs(saldoMinutos) / 60);
|
||||
const minutos = Math.abs(saldoMinutos) % 60;
|
||||
const positivo = saldoMinutos >= 0;
|
||||
|
||||
return {
|
||||
...registro,
|
||||
funcionario: funcionario
|
||||
@@ -324,6 +436,12 @@ export const listarRegistrosPeriodo = query({
|
||||
descricaoCargo: funcionario.descricaoCargo,
|
||||
}
|
||||
: null,
|
||||
saldoDiario: {
|
||||
saldoMinutos,
|
||||
horas,
|
||||
minutos,
|
||||
positivo,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -613,11 +731,20 @@ export const obterHistoricoESaldoDia = query({
|
||||
const horasTrabalhadas = calcularHorasTrabalhadas(registros);
|
||||
const saldoMinutos = horasTrabalhadas - cargaHorariaDiaria;
|
||||
|
||||
const horas = Math.floor(Math.abs(saldoMinutos) / 60);
|
||||
const minutos = Math.abs(saldoMinutos) % 60;
|
||||
const positivo = saldoMinutos >= 0;
|
||||
|
||||
return {
|
||||
registros,
|
||||
cargaHorariaDiaria,
|
||||
horasTrabalhadas,
|
||||
saldoMinutos,
|
||||
saldoFormatado: {
|
||||
horas,
|
||||
minutos,
|
||||
positivo,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -658,3 +785,550 @@ export const obterBancoHorasFuncionario = query({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper: Verificar se usuário é gestor do funcionário
|
||||
*/
|
||||
async function verificarGestorDoFuncionario(
|
||||
ctx: QueryCtx | MutationCtx,
|
||||
gestorId: Id<'usuarios'>,
|
||||
funcionarioId: Id<'funcionarios'>
|
||||
): Promise<boolean> {
|
||||
const membroTime = await ctx.db
|
||||
.query('timesMembros')
|
||||
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', funcionarioId))
|
||||
.filter((q) => q.eq(q.field('ativo'), true))
|
||||
.first();
|
||||
|
||||
if (!membroTime) return false;
|
||||
|
||||
const time = await ctx.db.get(membroTime.timeId);
|
||||
if (!time) return false;
|
||||
|
||||
return time.gestorId === gestorId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edita um registro de ponto (homologação pelo gestor)
|
||||
*/
|
||||
export const editarRegistroPonto = mutation({
|
||||
args: {
|
||||
registroId: v.id('registrosPonto'),
|
||||
horaNova: v.number(),
|
||||
minutoNova: v.number(),
|
||||
motivoId: v.optional(v.string()),
|
||||
motivoTipo: v.optional(v.string()),
|
||||
motivoDescricao: v.optional(v.string()),
|
||||
observacoes: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
// Buscar registro
|
||||
const registro = await ctx.db.get(args.registroId);
|
||||
if (!registro) {
|
||||
throw new Error('Registro não encontrado');
|
||||
}
|
||||
|
||||
// Verificar se é gestor do funcionário
|
||||
const isGestor = await verificarGestorDoFuncionario(ctx, usuario._id, registro.funcionarioId);
|
||||
if (!isGestor) {
|
||||
throw new Error('Você não tem permissão para editar este registro');
|
||||
}
|
||||
|
||||
// Salvar dados anteriores
|
||||
const horaAnterior = registro.hora;
|
||||
const minutoAnterior = registro.minuto;
|
||||
|
||||
// Atualizar registro
|
||||
await ctx.db.patch(args.registroId, {
|
||||
hora: args.horaNova,
|
||||
minuto: args.minutoNova,
|
||||
editadoPorGestor: true,
|
||||
});
|
||||
|
||||
// Criar registro de homologação
|
||||
const homologacaoId = await ctx.db.insert('homologacoesPonto', {
|
||||
registroId: args.registroId,
|
||||
funcionarioId: registro.funcionarioId,
|
||||
gestorId: usuario._id,
|
||||
horaAnterior,
|
||||
minutoAnterior,
|
||||
horaNova: args.horaNova,
|
||||
minutoNova: args.minutoNova,
|
||||
motivoId: args.motivoId,
|
||||
motivoTipo: args.motivoTipo,
|
||||
motivoDescricao: args.motivoDescricao,
|
||||
observacoes: args.observacoes,
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
|
||||
// Atualizar registro com ID da homologação
|
||||
await ctx.db.patch(args.registroId, {
|
||||
homologacaoId,
|
||||
});
|
||||
|
||||
// Recalcular banco de horas do dia
|
||||
const config = await ctx.db
|
||||
.query('configuracaoPonto')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
if (config) {
|
||||
await atualizarBancoHoras(ctx, registro.funcionarioId, registro.data, config);
|
||||
}
|
||||
|
||||
return { success: true, homologacaoId };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Ajusta banco de horas (compensar, abonar ou descontar)
|
||||
*/
|
||||
export const ajustarBancoHoras = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
tipoAjuste: v.union(v.literal('compensar'), v.literal('abonar'), v.literal('descontar')),
|
||||
periodoDias: v.number(),
|
||||
periodoHoras: v.number(),
|
||||
periodoMinutos: v.number(),
|
||||
motivoId: v.optional(v.string()),
|
||||
motivoTipo: v.optional(v.string()),
|
||||
motivoDescricao: v.optional(v.string()),
|
||||
observacoes: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
// Verificar se é gestor do funcionário
|
||||
const isGestor = await verificarGestorDoFuncionario(ctx, usuario._id, args.funcionarioId);
|
||||
if (!isGestor) {
|
||||
throw new Error('Você não tem permissão para ajustar banco de horas deste funcionário');
|
||||
}
|
||||
|
||||
// Calcular ajuste em minutos
|
||||
const ajusteMinutos =
|
||||
args.periodoDias * 24 * 60 + args.periodoHoras * 60 + args.periodoMinutos;
|
||||
|
||||
// Aplicar sinal baseado no tipo de ajuste
|
||||
let ajusteFinal = ajusteMinutos;
|
||||
if (args.tipoAjuste === 'descontar') {
|
||||
ajusteFinal = -ajusteMinutos;
|
||||
}
|
||||
|
||||
// Buscar banco de horas mais recente ou criar um registro de ajuste
|
||||
const hoje = new Date().toISOString().split('T')[0]!;
|
||||
const bancoHorasAtual = await ctx.db
|
||||
.query('bancoHoras')
|
||||
.withIndex('by_funcionario_data', (q) =>
|
||||
q.eq('funcionarioId', args.funcionarioId).eq('data', hoje)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (bancoHorasAtual) {
|
||||
// Atualizar saldo do dia atual
|
||||
await ctx.db.patch(bancoHorasAtual._id, {
|
||||
saldoMinutos: bancoHorasAtual.saldoMinutos + ajusteFinal,
|
||||
});
|
||||
} else {
|
||||
// Criar novo registro de banco de horas para o ajuste
|
||||
const config = await ctx.db
|
||||
.query('configuracaoPonto')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Configuração de ponto não encontrada');
|
||||
}
|
||||
|
||||
const cargaHorariaDiaria = calcularCargaHorariaDiaria(config);
|
||||
|
||||
await ctx.db.insert('bancoHoras', {
|
||||
funcionarioId: args.funcionarioId,
|
||||
data: hoje,
|
||||
cargaHorariaDiaria,
|
||||
horasTrabalhadas: 0,
|
||||
saldoMinutos: ajusteFinal,
|
||||
registrosPontoIds: [],
|
||||
calculadoEm: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Criar registro de homologação
|
||||
const homologacaoId = await ctx.db.insert('homologacoesPonto', {
|
||||
funcionarioId: args.funcionarioId,
|
||||
gestorId: usuario._id,
|
||||
motivoId: args.motivoId,
|
||||
motivoTipo: args.motivoTipo,
|
||||
motivoDescricao: args.motivoDescricao,
|
||||
observacoes: args.observacoes,
|
||||
tipoAjuste: args.tipoAjuste,
|
||||
periodoDias: args.periodoDias,
|
||||
periodoHoras: args.periodoHoras,
|
||||
periodoMinutos: args.periodoMinutos,
|
||||
ajusteMinutos: ajusteFinal,
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
|
||||
return { success: true, homologacaoId, ajusteMinutos: ajusteFinal };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Lista homologações de um funcionário ou time
|
||||
*/
|
||||
export const listarHomologacoes = query({
|
||||
args: {
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
let homologacoes;
|
||||
|
||||
if (args.funcionarioId) {
|
||||
// Verificar se é gestor do funcionário ou o próprio funcionário
|
||||
const funcionarioId = args.funcionarioId; // Garantir que não é undefined
|
||||
const isGestor = await verificarGestorDoFuncionario(ctx, usuario._id, funcionarioId);
|
||||
const isProprioFuncionario = usuario.funcionarioId === funcionarioId;
|
||||
|
||||
if (!isGestor && !isProprioFuncionario) {
|
||||
throw new Error('Você não tem permissão para ver estas homologações');
|
||||
}
|
||||
|
||||
homologacoes = await ctx.db
|
||||
.query('homologacoesPonto')
|
||||
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', funcionarioId))
|
||||
.order('desc')
|
||||
.collect();
|
||||
} else {
|
||||
// Listar homologações do gestor
|
||||
homologacoes = await ctx.db
|
||||
.query('homologacoesPonto')
|
||||
.withIndex('by_gestor', (q) => q.eq('gestorId', usuario._id))
|
||||
.order('desc')
|
||||
.collect();
|
||||
}
|
||||
|
||||
// Buscar informações adicionais
|
||||
const homologacoesComDetalhes = await Promise.all(
|
||||
homologacoes.map(async (h) => {
|
||||
const funcionario = await ctx.db.get(h.funcionarioId);
|
||||
const gestor = await ctx.db.get(h.gestorId);
|
||||
const registro = h.registroId ? await ctx.db.get(h.registroId) : null;
|
||||
|
||||
return {
|
||||
...h,
|
||||
funcionario: funcionario
|
||||
? {
|
||||
nome: funcionario.nome,
|
||||
matricula: funcionario.matricula,
|
||||
}
|
||||
: null,
|
||||
gestor: gestor
|
||||
? {
|
||||
nome: gestor.nome,
|
||||
}
|
||||
: null,
|
||||
registro: registro
|
||||
? {
|
||||
data: registro.data,
|
||||
tipo: registro.tipo,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return homologacoesComDetalhes;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obtém opções de motivos de atestados/declarações
|
||||
*/
|
||||
export const obterMotivosAtestados = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
// Buscar tipos de atestados e declarações
|
||||
const atestados = await ctx.db.query('atestados').collect();
|
||||
const tiposUnicos = new Set<string>();
|
||||
|
||||
atestados.forEach((a) => {
|
||||
if (a.cid) tiposUnicos.add(`CID: ${a.cid}`);
|
||||
if (a.observacoes) tiposUnicos.add(a.observacoes);
|
||||
});
|
||||
|
||||
return {
|
||||
tipos: Array.from(tiposUnicos),
|
||||
opcoesPadrao: [
|
||||
'Atestado Médico',
|
||||
'Declaração',
|
||||
'Ajuste Administrativo',
|
||||
'Compensação de Horas',
|
||||
'Abono',
|
||||
'Desconto em Folha',
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Cria uma dispensa de registro de ponto
|
||||
*/
|
||||
export const criarDispensaRegistro = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
dataInicio: v.string(), // YYYY-MM-DD
|
||||
horaInicio: v.number(),
|
||||
minutoInicio: v.number(),
|
||||
dataFim: v.string(), // YYYY-MM-DD
|
||||
horaFim: v.number(),
|
||||
minutoFim: v.number(),
|
||||
motivo: v.string(),
|
||||
isento: v.boolean(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
// Verificar se é gestor do funcionário
|
||||
const isGestor = await verificarGestorDoFuncionario(ctx, usuario._id, args.funcionarioId);
|
||||
if (!isGestor) {
|
||||
throw new Error('Você não tem permissão para criar dispensa para este funcionário');
|
||||
}
|
||||
|
||||
// Validar datas
|
||||
const dataInicioObj = new Date(args.dataInicio);
|
||||
const dataFimObj = new Date(args.dataFim);
|
||||
|
||||
if (dataFimObj < dataInicioObj) {
|
||||
throw new Error('Data fim deve ser maior ou igual à data início');
|
||||
}
|
||||
|
||||
// Criar dispensa
|
||||
const dispensaId = await ctx.db.insert('dispensasRegistro', {
|
||||
funcionarioId: args.funcionarioId,
|
||||
gestorId: usuario._id,
|
||||
dataInicio: args.dataInicio,
|
||||
horaInicio: args.horaInicio,
|
||||
minutoInicio: args.minutoInicio,
|
||||
dataFim: args.dataFim,
|
||||
horaFim: args.horaFim,
|
||||
minutoFim: args.minutoFim,
|
||||
motivo: args.motivo,
|
||||
isento: args.isento,
|
||||
ativo: true,
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
|
||||
return { success: true, dispensaId };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove uma dispensa de registro (cancela)
|
||||
*/
|
||||
export const removerDispensaRegistro = mutation({
|
||||
args: {
|
||||
dispensaId: v.id('dispensasRegistro'),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
const dispensa = await ctx.db.get(args.dispensaId);
|
||||
if (!dispensa) {
|
||||
throw new Error('Dispensa não encontrada');
|
||||
}
|
||||
|
||||
// Verificar se é gestor do funcionário
|
||||
const isGestor = await verificarGestorDoFuncionario(ctx, usuario._id, dispensa.funcionarioId);
|
||||
if (!isGestor && dispensa.gestorId !== usuario._id) {
|
||||
throw new Error('Você não tem permissão para remover esta dispensa');
|
||||
}
|
||||
|
||||
// Desativar dispensa
|
||||
await ctx.db.patch(args.dispensaId, {
|
||||
ativo: false,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Lista dispensas de registro
|
||||
*/
|
||||
export const listarDispensas = query({
|
||||
args: {
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
apenasAtivas: v.optional(v.boolean()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
let dispensas;
|
||||
|
||||
if (args.funcionarioId) {
|
||||
// Verificar se é gestor do funcionário ou o próprio funcionário
|
||||
const funcionarioId = args.funcionarioId; // Garantir que não é undefined
|
||||
const isGestor = await verificarGestorDoFuncionario(ctx, usuario._id, funcionarioId);
|
||||
const isProprioFuncionario = usuario.funcionarioId === funcionarioId;
|
||||
|
||||
if (!isGestor && !isProprioFuncionario) {
|
||||
throw new Error('Você não tem permissão para ver estas dispensas');
|
||||
}
|
||||
|
||||
dispensas = await ctx.db
|
||||
.query('dispensasRegistro')
|
||||
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', funcionarioId))
|
||||
.filter((q) => {
|
||||
if (args.apenasAtivas !== undefined && args.apenasAtivas) {
|
||||
return q.eq(q.field('ativo'), true);
|
||||
}
|
||||
return true; // Retornar todas se apenasAtivas não for especificado
|
||||
})
|
||||
.order('desc')
|
||||
.collect();
|
||||
} else {
|
||||
// Listar dispensas do gestor
|
||||
dispensas = await ctx.db
|
||||
.query('dispensasRegistro')
|
||||
.withIndex('by_gestor', (q) => q.eq('gestorId', usuario._id))
|
||||
.filter((q) => {
|
||||
if (args.apenasAtivas !== undefined && args.apenasAtivas) {
|
||||
return q.eq(q.field('ativo'), true);
|
||||
}
|
||||
return true; // Retornar todas se apenasAtivas não for especificado
|
||||
})
|
||||
.order('desc')
|
||||
.collect();
|
||||
}
|
||||
|
||||
// Buscar informações adicionais
|
||||
const dispensasComDetalhes = await Promise.all(
|
||||
dispensas.map(async (d) => {
|
||||
const funcionario = await ctx.db.get(d.funcionarioId);
|
||||
const gestor = await ctx.db.get(d.gestorId);
|
||||
|
||||
// Verificar se expirou (se não for isento)
|
||||
let expirada = false;
|
||||
if (!d.isento) {
|
||||
const agora = new Date();
|
||||
const dataFimTimestamp = new Date(
|
||||
`${d.dataFim}T${d.horaFim.toString().padStart(2, '0')}:${d.minutoFim.toString().padStart(2, '0')}:00`
|
||||
).getTime();
|
||||
expirada = agora.getTime() > dataFimTimestamp;
|
||||
}
|
||||
|
||||
return {
|
||||
...d,
|
||||
funcionario: funcionario
|
||||
? {
|
||||
nome: funcionario.nome,
|
||||
matricula: funcionario.matricula,
|
||||
}
|
||||
: null,
|
||||
gestor: gestor
|
||||
? {
|
||||
nome: gestor.nome,
|
||||
}
|
||||
: null,
|
||||
expirada,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return dispensasComDetalhes;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifica se funcionário está dispensado de registrar ponto em uma data/hora específica
|
||||
*/
|
||||
export const verificarDispensaAtiva = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
data: v.string(), // YYYY-MM-DD
|
||||
hora: v.optional(v.number()),
|
||||
minuto: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const dispensas = await ctx.db
|
||||
.query('dispensasRegistro')
|
||||
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId))
|
||||
.filter((q) => q.eq(q.field('ativo'), true))
|
||||
.collect();
|
||||
|
||||
const dataConsulta = new Date(args.data);
|
||||
|
||||
for (const dispensa of dispensas) {
|
||||
// Se for isento, sempre está dispensado
|
||||
if (dispensa.isento) {
|
||||
return {
|
||||
dispensado: true,
|
||||
dispensa,
|
||||
motivo: 'Isento de registro (caso excepcional)',
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se está no período
|
||||
const dataInicio = new Date(dispensa.dataInicio);
|
||||
const dataFim = new Date(dispensa.dataFim);
|
||||
|
||||
// Se a data está dentro do período
|
||||
if (dataConsulta >= dataInicio && dataConsulta <= dataFim) {
|
||||
// Se hora e minuto foram fornecidos, verificar também
|
||||
if (args.hora !== undefined && args.minuto !== undefined) {
|
||||
const timestampConsulta = new Date(
|
||||
`${args.data}T${args.hora.toString().padStart(2, '0')}:${args.minuto.toString().padStart(2, '0')}:00`
|
||||
).getTime();
|
||||
const timestampInicio = new Date(
|
||||
`${dispensa.dataInicio}T${dispensa.horaInicio.toString().padStart(2, '0')}:${dispensa.minutoInicio.toString().padStart(2, '0')}:00`
|
||||
).getTime();
|
||||
const timestampFim = new Date(
|
||||
`${dispensa.dataFim}T${dispensa.horaFim.toString().padStart(2, '0')}:${dispensa.minutoFim.toString().padStart(2, '0')}:00`
|
||||
).getTime();
|
||||
|
||||
if (timestampConsulta >= timestampInicio && timestampConsulta <= timestampFim) {
|
||||
return {
|
||||
dispensado: true,
|
||||
dispensa,
|
||||
motivo: dispensa.motivo,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Apenas verificar data
|
||||
return {
|
||||
dispensado: true,
|
||||
dispensa,
|
||||
motivo: dispensa.motivo,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dispensado: false,
|
||||
dispensa: null,
|
||||
motivo: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1390,6 +1390,10 @@ export default defineSchema({
|
||||
// Justificativa opcional para o registro
|
||||
justificativa: v.optional(v.string()),
|
||||
|
||||
// Campos para homologação
|
||||
editadoPorGestor: v.optional(v.boolean()),
|
||||
homologacaoId: v.optional(v.id("homologacoesPonto")),
|
||||
|
||||
criadoEm: v.number(),
|
||||
})
|
||||
.index("by_funcionario_data", ["funcionarioId", "data"])
|
||||
@@ -1443,4 +1447,60 @@ export default defineSchema({
|
||||
.index("by_funcionario_data", ["funcionarioId", "data"])
|
||||
.index("by_funcionario", ["funcionarioId"])
|
||||
.index("by_data", ["data"]),
|
||||
|
||||
// Homologações de Ponto - Edições e ajustes realizados pelo gestor
|
||||
homologacoesPonto: defineTable({
|
||||
registroId: v.optional(v.id("registrosPonto")), // ID do registro editado (se for edição)
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
gestorId: v.id("usuarios"),
|
||||
// Dados do registro original (se for edição)
|
||||
horaAnterior: v.optional(v.number()),
|
||||
minutoAnterior: v.optional(v.number()),
|
||||
// Dados do registro novo (se for edição)
|
||||
horaNova: v.optional(v.number()),
|
||||
minutoNova: v.optional(v.number()),
|
||||
// Motivo e observações
|
||||
motivoId: v.optional(v.string()), // ID do motivo (referência a atestados/declarações)
|
||||
motivoTipo: v.optional(v.string()), // Tipo do motivo (atestado, declaracao, etc)
|
||||
motivoDescricao: v.optional(v.string()), // Descrição do motivo
|
||||
observacoes: v.optional(v.string()),
|
||||
// Tipo de ajuste (se for ajuste de banco de horas)
|
||||
tipoAjuste: v.optional(v.union(
|
||||
v.literal("compensar"),
|
||||
v.literal("abonar"),
|
||||
v.literal("descontar")
|
||||
)),
|
||||
// Período do ajuste (se for ajuste de banco de horas)
|
||||
periodoDias: v.optional(v.number()),
|
||||
periodoHoras: v.optional(v.number()),
|
||||
periodoMinutos: v.optional(v.number()),
|
||||
// Ajuste em minutos (calculado)
|
||||
ajusteMinutos: v.optional(v.number()),
|
||||
criadoEm: v.number(),
|
||||
})
|
||||
.index("by_funcionario", ["funcionarioId"])
|
||||
.index("by_gestor", ["gestorId"])
|
||||
.index("by_registro", ["registroId"])
|
||||
.index("by_data", ["criadoEm"]),
|
||||
|
||||
// Dispensas de Registro - Períodos onde funcionário está dispensado de registrar ponto
|
||||
dispensasRegistro: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
gestorId: v.id("usuarios"),
|
||||
dataInicio: v.string(), // YYYY-MM-DD
|
||||
horaInicio: v.number(),
|
||||
minutoInicio: v.number(),
|
||||
dataFim: v.string(), // YYYY-MM-DD
|
||||
horaFim: v.number(),
|
||||
minutoFim: v.number(),
|
||||
motivo: v.string(),
|
||||
isento: v.boolean(), // Se true, não expira (casos excepcionais)
|
||||
ativo: v.boolean(),
|
||||
criadoEm: v.number(),
|
||||
})
|
||||
.index("by_funcionario", ["funcionarioId"])
|
||||
.index("by_gestor", ["gestorId"])
|
||||
.index("by_ativo", ["ativo"])
|
||||
.index("by_data_inicio", ["dataInicio"])
|
||||
.index("by_data_fim", ["dataFim"]),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user