- Added functionality to delete homologations, restricted to users with managerial permissions. - Introduced modals for viewing details of homologations and confirming deletions, enhancing user interaction. - Updated the backend to support homologation deletion, including necessary permission checks and data integrity management. - Enhanced the UI to display alerts for unassociated employees and active dispensas during point registration, improving user feedback and error handling.
1377 lines
39 KiB
TypeScript
1377 lines
39 KiB
TypeScript
import { v } from 'convex/values';
|
|
import { mutation, query } from './_generated/server';
|
|
import type { MutationCtx, QueryCtx } from './_generated/server';
|
|
import { getCurrentUserFunction } from './auth';
|
|
import type { Id } from './_generated/dataModel';
|
|
|
|
/**
|
|
* Gera URL para upload de imagem do ponto
|
|
*/
|
|
export const generateUploadUrl = mutation({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
return await ctx.storage.generateUploadUrl();
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Calcula se o registro está dentro do prazo baseado na configuração
|
|
* Se toleranciaMinutos for 0, desconsidera atrasos (sempre retorna true)
|
|
*/
|
|
function calcularStatusPonto(
|
|
hora: number,
|
|
minuto: number,
|
|
horarioConfigurado: string,
|
|
toleranciaMinutos: number
|
|
): boolean {
|
|
// Se tolerância for 0, desconsiderar atrasos (qualquer registro é válido)
|
|
if (toleranciaMinutos === 0) {
|
|
return true;
|
|
}
|
|
|
|
const [horaConfig, minutoConfig] = horarioConfigurado.split(':').map(Number);
|
|
const totalMinutosRegistro = hora * 60 + minuto;
|
|
const totalMinutosConfigurado = horaConfig * 60 + minutoConfig;
|
|
const diferenca = totalMinutosRegistro - totalMinutosConfigurado;
|
|
return diferenca <= toleranciaMinutos && diferenca >= -toleranciaMinutos;
|
|
}
|
|
|
|
/**
|
|
* Determina o tipo de registro baseado na sequência lógica
|
|
*/
|
|
async function determinarTipoRegistro(
|
|
ctx: QueryCtx | MutationCtx,
|
|
funcionarioId: Id<'funcionarios'>,
|
|
data: string
|
|
): Promise<'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida'> {
|
|
const registrosHoje = await ctx.db
|
|
.query('registrosPonto')
|
|
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
|
.order('desc')
|
|
.collect();
|
|
|
|
if (registrosHoje.length === 0) {
|
|
return 'entrada';
|
|
}
|
|
|
|
const ultimoRegistro = registrosHoje[0];
|
|
switch (ultimoRegistro.tipo) {
|
|
case 'entrada':
|
|
return 'saida_almoco';
|
|
case 'saida_almoco':
|
|
return 'retorno_almoco';
|
|
case 'retorno_almoco':
|
|
return 'saida';
|
|
case 'saida':
|
|
// Se já saiu, próximo registro é entrada (novo dia)
|
|
return 'entrada';
|
|
default:
|
|
return 'entrada';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registra um ponto (entrada, saída, etc.)
|
|
*/
|
|
export const registrarPonto = mutation({
|
|
args: {
|
|
imagemId: v.optional(v.id('_storage')),
|
|
informacoesDispositivo: v.optional(
|
|
v.object({
|
|
ipAddress: v.optional(v.string()),
|
|
ipPublico: v.optional(v.string()),
|
|
ipLocal: v.optional(v.string()),
|
|
userAgent: v.optional(v.string()),
|
|
browser: v.optional(v.string()),
|
|
browserVersion: v.optional(v.string()),
|
|
engine: v.optional(v.string()),
|
|
sistemaOperacional: v.optional(v.string()),
|
|
osVersion: v.optional(v.string()),
|
|
arquitetura: v.optional(v.string()),
|
|
plataforma: v.optional(v.string()),
|
|
latitude: v.optional(v.number()),
|
|
longitude: v.optional(v.number()),
|
|
precisao: v.optional(v.number()),
|
|
endereco: v.optional(v.string()),
|
|
cidade: v.optional(v.string()),
|
|
estado: v.optional(v.string()),
|
|
pais: v.optional(v.string()),
|
|
timezone: v.optional(v.string()),
|
|
deviceType: v.optional(v.string()),
|
|
deviceModel: v.optional(v.string()),
|
|
screenResolution: v.optional(v.string()),
|
|
coresTela: v.optional(v.number()),
|
|
idioma: v.optional(v.string()),
|
|
isMobile: v.optional(v.boolean()),
|
|
isTablet: v.optional(v.boolean()),
|
|
isDesktop: v.optional(v.boolean()),
|
|
connectionType: v.optional(v.string()),
|
|
memoryInfo: v.optional(v.string()),
|
|
})
|
|
),
|
|
timestamp: v.number(),
|
|
sincronizadoComServidor: v.boolean(),
|
|
justificativa: v.optional(v.string()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
if (!usuario.funcionarioId) {
|
|
throw new Error('Usuário não possui funcionário associado');
|
|
}
|
|
|
|
// Verificar se funcionário está ativo
|
|
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
|
if (!funcionario) {
|
|
throw new Error('Funcionário não encontrado');
|
|
}
|
|
|
|
// Obter configuração de ponto
|
|
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');
|
|
}
|
|
|
|
// Obter configuração de ponto para GMT offset (buscar configuração ativa)
|
|
const configPonto = await ctx.db
|
|
.query('configuracaoPonto')
|
|
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
|
.first();
|
|
|
|
// Converter timestamp para data/hora com ajuste de GMT
|
|
const gmtOffset = configPonto?.gmtOffset ?? 0;
|
|
const timestampAjustado = args.timestamp + (gmtOffset * 60 * 60 * 1000);
|
|
const dataObj = new Date(timestampAjustado);
|
|
const data = dataObj.toISOString().split('T')[0]!; // YYYY-MM-DD
|
|
const hora = dataObj.getUTCHours();
|
|
const minuto = dataObj.getUTCMinutes();
|
|
const segundo = dataObj.getUTCSeconds();
|
|
|
|
// Verificar se já existe registro no mesmo minuto
|
|
const funcionarioId = usuario.funcionarioId; // Já verificado acima, não é undefined
|
|
const registrosMinuto = await ctx.db
|
|
.query('registrosPonto')
|
|
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
|
.collect();
|
|
|
|
const registroDuplicado = registrosMinuto.find(
|
|
(r) => r.hora === hora && r.minuto === minuto
|
|
);
|
|
|
|
if (registroDuplicado) {
|
|
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);
|
|
|
|
// Calcular horário esperado e tolerância
|
|
let horarioConfigurado = '';
|
|
switch (tipo) {
|
|
case 'entrada':
|
|
horarioConfigurado = config.horarioEntrada;
|
|
break;
|
|
case 'saida_almoco':
|
|
horarioConfigurado = config.horarioSaidaAlmoco;
|
|
break;
|
|
case 'retorno_almoco':
|
|
horarioConfigurado = config.horarioRetornoAlmoco;
|
|
break;
|
|
case 'saida':
|
|
horarioConfigurado = config.horarioSaida;
|
|
break;
|
|
}
|
|
|
|
const dentroDoPrazo = calcularStatusPonto(hora, minuto, horarioConfigurado, config.toleranciaMinutos);
|
|
|
|
// Criar registro
|
|
const registroId = await ctx.db.insert('registrosPonto', {
|
|
funcionarioId: usuario.funcionarioId,
|
|
tipo,
|
|
data,
|
|
hora,
|
|
minuto,
|
|
segundo,
|
|
timestamp: args.timestamp,
|
|
imagemId: args.imagemId,
|
|
sincronizadoComServidor: args.sincronizadoComServidor,
|
|
toleranciaMinutos: config.toleranciaMinutos,
|
|
dentroDoPrazo,
|
|
justificativa: args.justificativa,
|
|
ipAddress: args.informacoesDispositivo?.ipAddress,
|
|
ipPublico: args.informacoesDispositivo?.ipPublico,
|
|
ipLocal: args.informacoesDispositivo?.ipLocal,
|
|
userAgent: args.informacoesDispositivo?.userAgent,
|
|
browser: args.informacoesDispositivo?.browser,
|
|
browserVersion: args.informacoesDispositivo?.browserVersion,
|
|
engine: args.informacoesDispositivo?.engine,
|
|
sistemaOperacional: args.informacoesDispositivo?.sistemaOperacional,
|
|
osVersion: args.informacoesDispositivo?.osVersion,
|
|
arquitetura: args.informacoesDispositivo?.arquitetura,
|
|
plataforma: args.informacoesDispositivo?.plataforma,
|
|
latitude: args.informacoesDispositivo?.latitude,
|
|
longitude: args.informacoesDispositivo?.longitude,
|
|
precisao: args.informacoesDispositivo?.precisao,
|
|
endereco: args.informacoesDispositivo?.endereco,
|
|
cidade: args.informacoesDispositivo?.cidade,
|
|
estado: args.informacoesDispositivo?.estado,
|
|
pais: args.informacoesDispositivo?.pais,
|
|
timezone: args.informacoesDispositivo?.timezone,
|
|
deviceType: args.informacoesDispositivo?.deviceType,
|
|
deviceModel: args.informacoesDispositivo?.deviceModel,
|
|
screenResolution: args.informacoesDispositivo?.screenResolution,
|
|
coresTela: args.informacoesDispositivo?.coresTela,
|
|
idioma: args.informacoesDispositivo?.idioma,
|
|
isMobile: args.informacoesDispositivo?.isMobile,
|
|
isTablet: args.informacoesDispositivo?.isTablet,
|
|
isDesktop: args.informacoesDispositivo?.isDesktop,
|
|
connectionType: args.informacoesDispositivo?.connectionType,
|
|
memoryInfo: args.informacoesDispositivo?.memoryInfo,
|
|
criadoEm: Date.now(),
|
|
});
|
|
|
|
// Atualizar banco de horas após registrar
|
|
await atualizarBancoHoras(ctx, usuario.funcionarioId, data, config);
|
|
|
|
return { registroId, tipo, dentroDoPrazo };
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Lista registros do dia atual do funcionário
|
|
*/
|
|
export const listarRegistrosDia = query({
|
|
args: {
|
|
data: v.optional(v.string()), // YYYY-MM-DD, se não fornecido usa hoje
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario || !usuario.funcionarioId) {
|
|
return [];
|
|
}
|
|
|
|
const funcionarioId = usuario.funcionarioId; // Garantir que não é undefined
|
|
const data = args.data || new Date().toISOString().split('T')[0]!;
|
|
|
|
const registros = await ctx.db
|
|
.query('registrosPonto')
|
|
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
|
.order('asc')
|
|
.collect();
|
|
|
|
return registros;
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 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)
|
|
*/
|
|
export const listarRegistrosPeriodo = query({
|
|
args: {
|
|
funcionarioId: v.optional(v.id('funcionarios')),
|
|
dataInicio: v.string(), // YYYY-MM-DD
|
|
dataFim: v.string(), // YYYY-MM-DD
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
// Verificar permissão (RH ou TI)
|
|
// Por enquanto, permitir se tiver funcionarioId ou for admin
|
|
// TODO: Implementar verificação de permissão adequada
|
|
|
|
const dataFim = new Date(args.dataFim);
|
|
dataFim.setHours(23, 59, 59, 999);
|
|
|
|
const registros = await ctx.db
|
|
.query('registrosPonto')
|
|
.withIndex('by_data', (q) => q.gte('data', args.dataInicio).lte('data', args.dataFim))
|
|
.collect();
|
|
|
|
// Filtrar por funcionário se especificado
|
|
let registrosFiltrados = registros;
|
|
if (args.funcionarioId) {
|
|
registrosFiltrados = registros.filter((r) => r.funcionarioId === args.funcionarioId);
|
|
}
|
|
|
|
// Buscar informações dos funcionários
|
|
const funcionariosIds = new Set(registrosFiltrados.map((r) => r.funcionarioId));
|
|
const funcionarios = await Promise.all(
|
|
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
|
|
? {
|
|
nome: funcionario.nome,
|
|
matricula: funcionario.matricula,
|
|
descricaoCargo: funcionario.descricaoCargo,
|
|
}
|
|
: null,
|
|
saldoDiario: {
|
|
saldoMinutos,
|
|
horas,
|
|
minutos,
|
|
positivo,
|
|
},
|
|
};
|
|
});
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Obtém estatísticas de pontos (para gráficos)
|
|
*/
|
|
export const obterEstatisticas = query({
|
|
args: {
|
|
dataInicio: v.string(), // YYYY-MM-DD
|
|
dataFim: v.string(), // YYYY-MM-DD
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
// TODO: Verificar permissão (RH ou TI)
|
|
|
|
const registros = await ctx.db
|
|
.query('registrosPonto')
|
|
.withIndex('by_data', (q) => q.gte('data', args.dataInicio).lte('data', args.dataFim))
|
|
.collect();
|
|
|
|
const totalRegistros = registros.length;
|
|
const dentroDoPrazo = registros.filter((r) => r.dentroDoPrazo).length;
|
|
const foraDoPrazo = totalRegistros - dentroDoPrazo;
|
|
|
|
// Agrupar por funcionário
|
|
const funcionariosUnicos = new Set(registros.map((r) => r.funcionarioId));
|
|
const totalFuncionarios = funcionariosUnicos.size;
|
|
|
|
// Funcionários com registros dentro do prazo
|
|
const funcionariosDentroPrazo = new Set(
|
|
registros.filter((r) => r.dentroDoPrazo).map((r) => r.funcionarioId)
|
|
).size;
|
|
|
|
// Funcionários com registros fora do prazo
|
|
const funcionariosForaPrazo = totalFuncionarios - funcionariosDentroPrazo;
|
|
|
|
return {
|
|
totalRegistros,
|
|
dentroDoPrazo,
|
|
foraDoPrazo,
|
|
totalFuncionarios,
|
|
funcionariosDentroPrazo,
|
|
funcionariosForaPrazo,
|
|
};
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Obtém um registro específico (para comprovante)
|
|
*/
|
|
export const obterRegistro = query({
|
|
args: {
|
|
registroId: v.id('registrosPonto'),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const registro = await ctx.db.get(args.registroId);
|
|
if (!registro) {
|
|
throw new Error('Registro não encontrado');
|
|
}
|
|
|
|
// Verificar se o usuário tem permissão (próprio registro ou RH/TI)
|
|
if (registro.funcionarioId !== usuario.funcionarioId) {
|
|
// TODO: Verificar se é RH ou TI
|
|
// Por enquanto, permitir
|
|
}
|
|
|
|
const funcionario = await ctx.db.get(registro.funcionarioId);
|
|
let simbolo = null;
|
|
if (funcionario) {
|
|
simbolo = await ctx.db.get(funcionario.simboloId);
|
|
}
|
|
|
|
// Obter URL da imagem se existir
|
|
let imagemUrl = null;
|
|
if (registro.imagemId) {
|
|
imagemUrl = await ctx.storage.getUrl(registro.imagemId);
|
|
}
|
|
|
|
return {
|
|
...registro,
|
|
imagemUrl,
|
|
funcionario: funcionario
|
|
? {
|
|
nome: funcionario.nome,
|
|
matricula: funcionario.matricula,
|
|
descricaoCargo: funcionario.descricaoCargo,
|
|
simbolo: simbolo
|
|
? {
|
|
nome: simbolo.nome,
|
|
tipo: simbolo.tipo,
|
|
}
|
|
: null,
|
|
}
|
|
: null,
|
|
};
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Calcula carga horária diária esperada em minutos
|
|
*/
|
|
function calcularCargaHorariaDiaria(config: {
|
|
horarioEntrada: string;
|
|
horarioSaidaAlmoco: string;
|
|
horarioRetornoAlmoco: string;
|
|
horarioSaida: string;
|
|
}): number {
|
|
const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number);
|
|
const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco.split(':').map(Number);
|
|
const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco.split(':').map(Number);
|
|
const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number);
|
|
|
|
const minutosEntrada = horaEntrada * 60 + minutoEntrada;
|
|
const minutosSaidaAlmoco = horaSaidaAlmoco * 60 + minutoSaidaAlmoco;
|
|
const minutosRetornoAlmoco = horaRetornoAlmoco * 60 + minutoRetornoAlmoco;
|
|
const minutosSaida = horaSaida * 60 + minutoSaida;
|
|
|
|
// Calcular horas trabalhadas: (saída almoço - entrada) + (saída - retorno almoço)
|
|
const horasManha = minutosSaidaAlmoco - minutosEntrada;
|
|
const horasTarde = minutosSaida - minutosRetornoAlmoco;
|
|
|
|
return horasManha + horasTarde;
|
|
}
|
|
|
|
/**
|
|
* Calcula horas trabalhadas do dia baseado nos registros
|
|
*/
|
|
function calcularHorasTrabalhadas(registros: Array<{
|
|
tipo: string;
|
|
hora: number;
|
|
minuto: number;
|
|
}>): number {
|
|
// Ordenar registros por timestamp
|
|
const registrosOrdenados = [...registros].sort((a, b) => {
|
|
const minutosA = a.hora * 60 + a.minuto;
|
|
const minutosB = b.hora * 60 + b.minuto;
|
|
return minutosA - minutosB;
|
|
});
|
|
|
|
let horasTrabalhadas = 0;
|
|
|
|
// Procurar entrada e saída
|
|
const entrada = registrosOrdenados.find((r) => r.tipo === 'entrada');
|
|
const saida = registrosOrdenados.find((r) => r.tipo === 'saida');
|
|
|
|
if (entrada && saida) {
|
|
const minutosEntrada = entrada.hora * 60 + entrada.minuto;
|
|
const minutosSaida = saida.hora * 60 + saida.minuto;
|
|
|
|
// Procurar saída e retorno do almoço
|
|
const saidaAlmoco = registrosOrdenados.find((r) => r.tipo === 'saida_almoco');
|
|
const retornoAlmoco = registrosOrdenados.find((r) => r.tipo === 'retorno_almoco');
|
|
|
|
if (saidaAlmoco && retornoAlmoco) {
|
|
// Tem intervalo de almoço: (saída almoço - entrada) + (saída - retorno almoço)
|
|
const minutosSaidaAlmoco = saidaAlmoco.hora * 60 + saidaAlmoco.minuto;
|
|
const minutosRetornoAlmoco = retornoAlmoco.hora * 60 + retornoAlmoco.minuto;
|
|
|
|
const horasManha = minutosSaidaAlmoco - minutosEntrada;
|
|
const horasTarde = minutosSaida - minutosRetornoAlmoco;
|
|
horasTrabalhadas = horasManha + horasTarde;
|
|
} else {
|
|
// Sem intervalo de almoço registrado: saída - entrada
|
|
horasTrabalhadas = minutosSaida - minutosEntrada;
|
|
}
|
|
}
|
|
|
|
return horasTrabalhadas;
|
|
}
|
|
|
|
/**
|
|
* Atualiza ou cria registro de banco de horas para o dia
|
|
*/
|
|
async function atualizarBancoHoras(
|
|
ctx: MutationCtx,
|
|
funcionarioId: Id<'funcionarios'>,
|
|
data: string,
|
|
config: {
|
|
horarioEntrada: string;
|
|
horarioSaidaAlmoco: string;
|
|
horarioRetornoAlmoco: string;
|
|
horarioSaida: string;
|
|
}
|
|
): Promise<void> {
|
|
// Buscar todos os registros do dia
|
|
const registrosDoDia = await ctx.db
|
|
.query('registrosPonto')
|
|
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
|
.collect();
|
|
|
|
// Calcular carga horária esperada
|
|
const cargaHorariaDiaria = calcularCargaHorariaDiaria(config);
|
|
|
|
// Calcular horas trabalhadas
|
|
const horasTrabalhadas = calcularHorasTrabalhadas(registrosDoDia);
|
|
|
|
// Calcular saldo (positivo = horas extras, negativo = déficit)
|
|
const saldoMinutos = horasTrabalhadas - cargaHorariaDiaria;
|
|
|
|
// Buscar banco de horas existente
|
|
const bancoHorasExistente = await ctx.db
|
|
.query('bancoHoras')
|
|
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
|
.first();
|
|
|
|
const registrosPontoIds = registrosDoDia.map((r) => r._id);
|
|
|
|
if (bancoHorasExistente) {
|
|
// Atualizar existente
|
|
await ctx.db.patch(bancoHorasExistente._id, {
|
|
cargaHorariaDiaria,
|
|
horasTrabalhadas,
|
|
saldoMinutos,
|
|
registrosPontoIds,
|
|
calculadoEm: Date.now(),
|
|
});
|
|
} else {
|
|
// Criar novo
|
|
await ctx.db.insert('bancoHoras', {
|
|
funcionarioId,
|
|
data,
|
|
cargaHorariaDiaria,
|
|
horasTrabalhadas,
|
|
saldoMinutos,
|
|
registrosPontoIds,
|
|
calculadoEm: Date.now(),
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtém histórico e saldo do dia
|
|
*/
|
|
export const obterHistoricoESaldoDia = query({
|
|
args: {
|
|
funcionarioId: v.id('funcionarios'),
|
|
data: v.string(), // YYYY-MM-DD
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario || !usuario.funcionarioId) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
// Verificar se é o próprio funcionário ou tem permissão
|
|
if (usuario.funcionarioId !== args.funcionarioId) {
|
|
// TODO: Verificar permissão de RH
|
|
}
|
|
|
|
// Buscar registros do dia
|
|
const registros = await ctx.db
|
|
.query('registrosPonto')
|
|
.withIndex('by_funcionario_data', (q) =>
|
|
q.eq('funcionarioId', args.funcionarioId).eq('data', args.data)
|
|
)
|
|
.order('asc')
|
|
.collect();
|
|
|
|
// Buscar configuração de ponto
|
|
const config = await ctx.db
|
|
.query('configuracaoPonto')
|
|
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
|
.first();
|
|
|
|
if (!config) {
|
|
return {
|
|
registros: [],
|
|
cargaHorariaDiaria: 0,
|
|
horasTrabalhadas: 0,
|
|
saldoMinutos: 0,
|
|
};
|
|
}
|
|
|
|
// Calcular valores
|
|
const cargaHorariaDiaria = calcularCargaHorariaDiaria(config);
|
|
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,
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Obtém banco de horas acumulado do funcionário
|
|
*/
|
|
export const obterBancoHorasFuncionario = query({
|
|
args: {
|
|
funcionarioId: v.id('funcionarios'),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
// Verificar se é o próprio funcionário ou tem permissão
|
|
if (usuario.funcionarioId !== args.funcionarioId) {
|
|
// TODO: Verificar permissão de RH
|
|
}
|
|
|
|
// Buscar todos os registros de banco de horas do funcionário
|
|
const bancosHoras = await ctx.db
|
|
.query('bancoHoras')
|
|
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId))
|
|
.order('desc')
|
|
.collect();
|
|
|
|
// Calcular saldo acumulado
|
|
const saldoAcumuladoMinutos = bancosHoras.reduce((acc, bh) => acc + bh.saldoMinutos, 0);
|
|
|
|
return {
|
|
bancosHoras,
|
|
saldoAcumuladoMinutos,
|
|
totalDias: bancosHoras.length,
|
|
};
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Exclui uma homologação (apenas para gestores)
|
|
*/
|
|
export const excluirHomologacao = mutation({
|
|
args: {
|
|
homologacaoId: v.id('homologacoesPonto'),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const homologacao = await ctx.db.get(args.homologacaoId);
|
|
if (!homologacao) {
|
|
throw new Error('Homologação não encontrada');
|
|
}
|
|
|
|
// Verificar se é gestor do funcionário
|
|
const isGestor = await verificarGestorDoFuncionario(ctx, usuario._id, homologacao.funcionarioId);
|
|
if (!isGestor && homologacao.gestorId !== usuario._id) {
|
|
throw new Error('Você não tem permissão para excluir esta homologação');
|
|
}
|
|
|
|
// Se a homologação estiver vinculada a um registro, remover a referência
|
|
if (homologacao.registroId) {
|
|
const registro = await ctx.db.get(homologacao.registroId);
|
|
if (registro && registro.homologacaoId === args.homologacaoId) {
|
|
await ctx.db.patch(homologacao.registroId, {
|
|
homologacaoId: undefined,
|
|
editadoPorGestor: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Excluir homologação
|
|
await ctx.db.delete(args.homologacaoId);
|
|
|
|
return { success: true };
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 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,
|
|
};
|
|
},
|
|
});
|
|
|