- Added functionality to customize labels for point registration types (Entrada, Saída, etc.) in the configuration settings. - Introduced a GMT offset adjustment feature to account for time zone differences during point registration. - Updated the backend to ensure default values for custom labels and GMT offset are set correctly. - Enhanced the UI to allow users to input and save personalized names for each type of point registration. - Improved the point registration process to utilize the new configuration settings for displaying labels consistently across the application.
661 lines
19 KiB
TypeScript
661 lines
19 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');
|
|
}
|
|
|
|
// 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;
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 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))
|
|
);
|
|
|
|
return registrosFiltrados.map((registro) => {
|
|
const funcionario = funcionarios.find((f) => f?._id === registro.funcionarioId);
|
|
return {
|
|
...registro,
|
|
funcionario: funcionario
|
|
? {
|
|
nome: funcionario.nome,
|
|
matricula: funcionario.matricula,
|
|
descricaoCargo: funcionario.descricaoCargo,
|
|
}
|
|
: null,
|
|
};
|
|
});
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 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;
|
|
|
|
return {
|
|
registros,
|
|
cargaHorariaDiaria,
|
|
horasTrabalhadas,
|
|
saldoMinutos,
|
|
};
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 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,
|
|
};
|
|
},
|
|
});
|
|
|