feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.
This commit is contained in:
@@ -1,20 +1,15 @@
|
||||
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';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import { validarLocalizacaoGeofencingInternal } from './enderecosMarcacao';
|
||||
|
||||
/**
|
||||
* Calcula distância entre duas coordenadas (fórmula de Haversine)
|
||||
* Retorna distância em metros
|
||||
*/
|
||||
function calcularDistancia(
|
||||
lat1: number,
|
||||
lon1: number,
|
||||
lat2: number,
|
||||
lon2: number
|
||||
): number {
|
||||
function calcularDistancia(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||
const R = 6371000; // Raio da Terra em metros
|
||||
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
||||
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
||||
@@ -97,10 +92,10 @@ async function validarLocalizacao(
|
||||
const avisos: string[] = [];
|
||||
let scoreConfianca = confiabilidadeGPS || 0.5;
|
||||
let valida = true;
|
||||
let distanciaIPvsGPS: number | undefined = undefined;
|
||||
let velocidadeUltimoRegistro: number | undefined = undefined;
|
||||
let distanciaUltimoRegistro: number | undefined = undefined;
|
||||
let tempoDecorridoHoras: number | undefined = undefined;
|
||||
let distanciaIPvsGPS: number | undefined;
|
||||
let velocidadeUltimoRegistro: number | undefined;
|
||||
let distanciaUltimoRegistro: number | undefined;
|
||||
let tempoDecorridoHoras: number | undefined;
|
||||
|
||||
// 1. Validar coordenadas básicas
|
||||
if (
|
||||
@@ -127,12 +122,7 @@ async function validarLocalizacao(
|
||||
if (ipAddress) {
|
||||
const ipGeo = await obterGeoPorIP(ipAddress);
|
||||
if (ipGeo) {
|
||||
distanciaIPvsGPS = calcularDistancia(
|
||||
latitude,
|
||||
longitude,
|
||||
ipGeo.latitude,
|
||||
ipGeo.longitude
|
||||
);
|
||||
distanciaIPvsGPS = calcularDistancia(latitude, longitude, ipGeo.latitude, ipGeo.longitude);
|
||||
|
||||
// Se diferença > 50km, muito suspeito
|
||||
if (distanciaIPvsGPS > 50000) {
|
||||
@@ -176,7 +166,7 @@ async function validarLocalizacao(
|
||||
|
||||
// Calcular velocidade (km/h) se tempo decorrido > 0
|
||||
if (tempoDecorridoHoras > 0 && tempoDecorridoHoras < 24) {
|
||||
velocidadeUltimoRegistro = (distanciaUltimoRegistro / 1000) / tempoDecorridoHoras; // km/h
|
||||
velocidadeUltimoRegistro = distanciaUltimoRegistro / 1000 / tempoDecorridoHoras; // km/h
|
||||
|
||||
// Se velocidade > 1000 km/h, impossível (mais rápido que avião)
|
||||
if (velocidadeUltimoRegistro > 1000) {
|
||||
@@ -280,19 +270,34 @@ function validarAcelerometro(
|
||||
// Se há dados de acelerômetro, validar
|
||||
if (acelerometroX !== undefined && acelerometroY !== undefined && acelerometroZ !== undefined) {
|
||||
// Verificar se valores são realistas (aceleração geralmente entre -20 e +20 m/s² em uso normal)
|
||||
const magnitude = magnitudeMovimento || Math.sqrt(acelerometroX * acelerometroX + acelerometroY * acelerometroY + acelerometroZ * acelerometroZ);
|
||||
|
||||
const magnitude =
|
||||
magnitudeMovimento ||
|
||||
Math.sqrt(
|
||||
acelerometroX * acelerometroX +
|
||||
acelerometroY * acelerometroY +
|
||||
acelerometroZ * acelerometroZ
|
||||
);
|
||||
|
||||
if (magnitude > 50) {
|
||||
// Aceleração muito alta pode indicar leitura errada ou emulador
|
||||
scoreConfianca *= 0.6;
|
||||
avisos.push(`Magnitude de movimento muito alta (${magnitude.toFixed(2)} m/s²). Pode indicar leitura incorreta.`);
|
||||
avisos.push(
|
||||
`Magnitude de movimento muito alta (${magnitude.toFixed(2)} m/s²). Pode indicar leitura incorreta.`
|
||||
);
|
||||
}
|
||||
|
||||
// Se não há movimento detectado quando deveria haver (em móvel), pode ser suspeito
|
||||
if (isDesktop !== true && movimentoDetectado === false && variacaoAcelerometro !== undefined && variacaoAcelerometro < 0.001) {
|
||||
if (
|
||||
isDesktop !== true &&
|
||||
movimentoDetectado === false &&
|
||||
variacaoAcelerometro !== undefined &&
|
||||
variacaoAcelerometro < 0.001
|
||||
) {
|
||||
// Variância muito baixa pode indicar que o dispositivo está parado ou emulador
|
||||
scoreConfianca *= 0.9;
|
||||
avisos.push('Nenhum movimento detectado durante o registro. Pode ser normal se o dispositivo estava parado.');
|
||||
avisos.push(
|
||||
'Nenhum movimento detectado durante o registro. Pode ser normal se o dispositivo estava parado.'
|
||||
);
|
||||
}
|
||||
|
||||
// Se há movimento, aumenta confiança
|
||||
@@ -320,7 +325,7 @@ export const generateUploadUrl = mutation({
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
return await ctx.storage.generateUploadUrl();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -434,21 +439,21 @@ export const registrarPonto = mutation({
|
||||
movimentoDetectado: v.boolean(),
|
||||
magnitude: v.number(),
|
||||
variacao: v.number(),
|
||||
timestamp: v.number(),
|
||||
timestamp: v.number()
|
||||
})
|
||||
),
|
||||
giroscopio: v.optional(
|
||||
v.object({
|
||||
alpha: v.number(),
|
||||
beta: v.number(),
|
||||
gamma: v.number(),
|
||||
gamma: v.number()
|
||||
})
|
||||
),
|
||||
)
|
||||
})
|
||||
),
|
||||
timestamp: v.number(),
|
||||
sincronizadoComServidor: v.boolean(),
|
||||
justificativa: v.optional(v.string()),
|
||||
justificativa: v.optional(v.string())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -487,7 +492,7 @@ export const registrarPonto = mutation({
|
||||
const hora = dataObj.getUTCHours();
|
||||
const minuto = dataObj.getUTCMinutes();
|
||||
const segundo = dataObj.getUTCSeconds();
|
||||
|
||||
|
||||
// Obter data no formato YYYY-MM-DD usando UTC
|
||||
const ano = dataObj.getUTCFullYear();
|
||||
const mes = String(dataObj.getUTCMonth() + 1).padStart(2, '0');
|
||||
@@ -498,12 +503,12 @@ export const registrarPonto = mutation({
|
||||
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))
|
||||
.withIndex('by_funcionario_data', (q) =>
|
||||
q.eq('funcionarioId', funcionarioId).eq('data', data)
|
||||
)
|
||||
.collect();
|
||||
|
||||
const registroDuplicado = registrosMinuto.find(
|
||||
(r) => r.hora === hora && r.minuto === minuto
|
||||
);
|
||||
const registroDuplicado = registrosMinuto.find((r) => r.hora === hora && r.minuto === minuto);
|
||||
|
||||
if (registroDuplicado) {
|
||||
throw new Error('Já existe um registro neste minuto');
|
||||
@@ -553,7 +558,7 @@ export const registrarPonto = mutation({
|
||||
if (agora.getTime() > dataFimTimestamp && !dispensa.isento) {
|
||||
// Desativar dispensa expirada (mutation pode fazer isso)
|
||||
await ctx.db.patch(dispensa._id, {
|
||||
ativo: false,
|
||||
ativo: false
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -578,7 +583,12 @@ export const registrarPonto = mutation({
|
||||
break;
|
||||
}
|
||||
|
||||
const dentroDoPrazo = calcularStatusPonto(hora, minuto, horarioConfigurado, config.toleranciaMinutos);
|
||||
const dentroDoPrazo = calcularStatusPonto(
|
||||
hora,
|
||||
minuto,
|
||||
horarioConfigurado,
|
||||
config.toleranciaMinutos
|
||||
);
|
||||
|
||||
// Validar localização se fornecida e salvar informações detalhadas
|
||||
let validacaoLocalizacao: {
|
||||
@@ -592,10 +602,7 @@ export const registrarPonto = mutation({
|
||||
tempoDecorridoHoras?: number;
|
||||
} | null = null;
|
||||
|
||||
if (
|
||||
args.informacoesDispositivo?.latitude &&
|
||||
args.informacoesDispositivo?.longitude
|
||||
) {
|
||||
if (args.informacoesDispositivo?.latitude && args.informacoesDispositivo?.longitude) {
|
||||
validacaoLocalizacao = await validarLocalizacao(
|
||||
ctx,
|
||||
usuario.funcionarioId,
|
||||
@@ -612,16 +619,19 @@ export const registrarPonto = mutation({
|
||||
const baixaConfianca = validacaoLocalizacao.scoreConfianca < 0.5;
|
||||
|
||||
if (suspeitaFrontend || suspeitaBackend || baixaConfianca) {
|
||||
console.warn('⚠️ LOCALIZAÇÃO COM BAIXA CONFIABILIDADE DETECTADA (registrando normalmente):', {
|
||||
funcionarioId: usuario.funcionarioId,
|
||||
latitude: args.informacoesDispositivo.latitude,
|
||||
longitude: args.informacoesDispositivo.longitude,
|
||||
confiabilidadeGPSFrontend: args.informacoesDispositivo.confiabilidadeGPS,
|
||||
scoreConfiancaBackend: validacaoLocalizacao.scoreConfianca,
|
||||
suspeitaFrontend: suspeitaFrontend ? args.informacoesDispositivo.motivoSuspeita : null,
|
||||
suspeitaBackend: suspeitaBackend ? validacaoLocalizacao.motivo : null,
|
||||
avisos: validacaoLocalizacao.avisos
|
||||
});
|
||||
console.warn(
|
||||
'⚠️ LOCALIZAÇÃO COM BAIXA CONFIABILIDADE DETECTADA (registrando normalmente):',
|
||||
{
|
||||
funcionarioId: usuario.funcionarioId,
|
||||
latitude: args.informacoesDispositivo.latitude,
|
||||
longitude: args.informacoesDispositivo.longitude,
|
||||
confiabilidadeGPSFrontend: args.informacoesDispositivo.confiabilidadeGPS,
|
||||
scoreConfiancaBackend: validacaoLocalizacao.scoreConfianca,
|
||||
suspeitaFrontend: suspeitaFrontend ? args.informacoesDispositivo.motivoSuspeita : null,
|
||||
suspeitaBackend: suspeitaBackend ? validacaoLocalizacao.motivo : null,
|
||||
avisos: validacaoLocalizacao.avisos
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,17 +666,14 @@ export const registrarPonto = mutation({
|
||||
validacaoLocalizacao = {
|
||||
valida: true,
|
||||
scoreConfianca: 1,
|
||||
avisos: [],
|
||||
avisos: []
|
||||
};
|
||||
}
|
||||
validacaoLocalizacao.avisos.push(...geofencing.avisos);
|
||||
|
||||
|
||||
// Reduzir score de confiança se estiver fora do raio
|
||||
if (!geofencing.dentroRaio) {
|
||||
validacaoLocalizacao.scoreConfianca = Math.min(
|
||||
validacaoLocalizacao.scoreConfianca,
|
||||
0.7
|
||||
);
|
||||
validacaoLocalizacao.scoreConfianca = Math.min(validacaoLocalizacao.scoreConfianca, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -697,7 +704,8 @@ export const registrarPonto = mutation({
|
||||
let scoreFinalConfianca = 1.0;
|
||||
if (validacaoLocalizacao && validacaoAcelerometro) {
|
||||
// GPS tem peso 0.7, acelerômetro tem peso 0.3
|
||||
scoreFinalConfianca = (validacaoLocalizacao.scoreConfianca * 0.7) + (validacaoAcelerometro.scoreConfianca * 0.3);
|
||||
scoreFinalConfianca =
|
||||
validacaoLocalizacao.scoreConfianca * 0.7 + validacaoAcelerometro.scoreConfianca * 0.3;
|
||||
} else if (validacaoLocalizacao) {
|
||||
scoreFinalConfianca = validacaoLocalizacao.scoreConfianca;
|
||||
} else if (validacaoAcelerometro) {
|
||||
@@ -738,8 +746,19 @@ export const registrarPonto = mutation({
|
||||
speed: args.informacoesDispositivo?.speed,
|
||||
confiabilidadeGPS: args.informacoesDispositivo?.confiabilidadeGPS,
|
||||
scoreConfiancaBackend: scoreFinalConfianca,
|
||||
suspeitaSpoofing: args.informacoesDispositivo?.suspeitaSpoofing || (validacaoLocalizacao ? validacaoLocalizacao.scoreConfianca < 0.5 || !validacaoLocalizacao.valida : undefined) || (validacaoAcelerometro ? validacaoAcelerometro.scoreConfianca < 0.5 || !validacaoAcelerometro.valida : undefined),
|
||||
motivoSuspeita: args.informacoesDispositivo?.motivoSuspeita || validacaoLocalizacao?.motivo || validacaoAcelerometro?.motivo || (todosAvisos.length > 0 ? todosAvisos.join('; ') : undefined),
|
||||
suspeitaSpoofing:
|
||||
args.informacoesDispositivo?.suspeitaSpoofing ||
|
||||
(validacaoLocalizacao
|
||||
? validacaoLocalizacao.scoreConfianca < 0.5 || !validacaoLocalizacao.valida
|
||||
: undefined) ||
|
||||
(validacaoAcelerometro
|
||||
? validacaoAcelerometro.scoreConfianca < 0.5 || !validacaoAcelerometro.valida
|
||||
: undefined),
|
||||
motivoSuspeita:
|
||||
args.informacoesDispositivo?.motivoSuspeita ||
|
||||
validacaoLocalizacao?.motivo ||
|
||||
validacaoAcelerometro?.motivo ||
|
||||
(todosAvisos.length > 0 ? todosAvisos.join('; ') : undefined),
|
||||
// Informações detalhadas de validação (sempre salvar quando houver validação)
|
||||
avisosValidacao: todosAvisos.length > 0 ? todosAvisos : undefined,
|
||||
// Informações de Geofencing
|
||||
@@ -775,14 +794,14 @@ export const registrarPonto = mutation({
|
||||
giroscopioGamma: args.informacoesDispositivo?.giroscopio?.gamma,
|
||||
sensorDisponivel: args.informacoesDispositivo?.sensorDisponivel,
|
||||
permissaoSensorNegada: args.informacoesDispositivo?.permissaoNegada,
|
||||
criadoEm: Date.now(),
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
// Atualizar banco de horas após registrar
|
||||
await atualizarBancoHoras(ctx, usuario.funcionarioId, data, config);
|
||||
|
||||
return { registroId, tipo, dentroDoPrazo };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -791,7 +810,7 @@ export const registrarPonto = mutation({
|
||||
export const listarRegistrosDia = query({
|
||||
args: {
|
||||
data: v.optional(v.string()), // YYYY-MM-DD, se não fornecido usa hoje
|
||||
_refresh: v.optional(v.number()), // Parâmetro usado pelo frontend para forçar refresh
|
||||
_refresh: v.optional(v.number()) // Parâmetro usado pelo frontend para forçar refresh
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -802,24 +821,33 @@ export const listarRegistrosDia = query({
|
||||
const funcionarioId = usuario.funcionarioId; // Garantir que não é undefined
|
||||
const data = args.data || new Date().toISOString().split('T')[0]!;
|
||||
|
||||
console.log('[listarRegistrosDia] Buscando registros:', { funcionarioId, data });
|
||||
console.log('[listarRegistrosDia] Buscando registros:', {
|
||||
funcionarioId,
|
||||
data
|
||||
});
|
||||
|
||||
const registros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
||||
.withIndex('by_funcionario_data', (q) =>
|
||||
q.eq('funcionarioId', funcionarioId).eq('data', data)
|
||||
)
|
||||
.order('asc')
|
||||
.collect();
|
||||
|
||||
console.log('[listarRegistrosDia] Registros encontrados:', registros.length, registros.map(r => ({
|
||||
_id: r._id,
|
||||
tipo: r.tipo,
|
||||
data: r.data,
|
||||
hora: r.hora,
|
||||
minuto: r.minuto
|
||||
})));
|
||||
console.log(
|
||||
'[listarRegistrosDia] Registros encontrados:',
|
||||
registros.length,
|
||||
registros.map((r) => ({
|
||||
_id: r._id,
|
||||
tipo: r.tipo,
|
||||
data: r.data,
|
||||
hora: r.hora,
|
||||
minuto: r.minuto
|
||||
}))
|
||||
);
|
||||
|
||||
return registros;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -828,7 +856,7 @@ export const listarRegistrosDia = query({
|
||||
export const obterSaldoDiario = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
data: v.string(), // YYYY-MM-DD
|
||||
data: v.string() // YYYY-MM-DD
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar banco de horas do dia
|
||||
@@ -844,7 +872,7 @@ export const obterSaldoDiario = query({
|
||||
saldoMinutos: 0,
|
||||
horas: 0,
|
||||
minutos: 0,
|
||||
positivo: true,
|
||||
positivo: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -856,9 +884,9 @@ export const obterSaldoDiario = query({
|
||||
saldoMinutos: bancoHoras.saldoMinutos,
|
||||
horas,
|
||||
minutos,
|
||||
positivo,
|
||||
positivo
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -868,7 +896,7 @@ export const listarRegistrosPeriodo = query({
|
||||
args: {
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
dataInicio: v.string(), // YYYY-MM-DD
|
||||
dataFim: v.string(), // YYYY-MM-DD
|
||||
dataFim: v.string() // YYYY-MM-DD
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -890,9 +918,9 @@ export const listarRegistrosPeriodo = query({
|
||||
// Validar formato YYYY-MM-DD
|
||||
const dataInicioRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dataInicioRegex.test(args.dataInicio) || !dataInicioRegex.test(args.dataFim)) {
|
||||
console.warn('[listarRegistrosPeriodo] Formato de data inválido', {
|
||||
dataInicio: args.dataInicio,
|
||||
dataFim: args.dataFim
|
||||
console.warn('[listarRegistrosPeriodo] Formato de data inválido', {
|
||||
dataInicio: args.dataInicio,
|
||||
dataFim: args.dataFim
|
||||
});
|
||||
return [];
|
||||
}
|
||||
@@ -905,18 +933,18 @@ export const listarRegistrosPeriodo = query({
|
||||
});
|
||||
|
||||
let registrosFiltrados;
|
||||
|
||||
|
||||
// Se funcionário foi especificado, usar índice por funcionário e data (mais eficiente)
|
||||
if (args.funcionarioId) {
|
||||
// Garantir que funcionarioId não é undefined para TypeScript
|
||||
const funcionarioId = args.funcionarioId;
|
||||
|
||||
|
||||
// Buscar todos os registros do funcionário
|
||||
const todosRegistrosFuncionario = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId))
|
||||
.collect();
|
||||
|
||||
|
||||
// Filtrar por período de data usando comparação de strings (formato YYYY-MM-DD)
|
||||
registrosFiltrados = todosRegistrosFuncionario.filter((r) => {
|
||||
// Comparação de strings funciona para formato YYYY-MM-DD
|
||||
@@ -928,47 +956,47 @@ export const listarRegistrosPeriodo = query({
|
||||
// Tentar usar índice por data primeiro
|
||||
const registros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_data', (q) =>
|
||||
q.gte('data', args.dataInicio).lte('data', args.dataFim)
|
||||
)
|
||||
.withIndex('by_data', (q) => q.gte('data', args.dataInicio).lte('data', args.dataFim))
|
||||
.collect();
|
||||
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Registros do índice by_data:', registros.length);
|
||||
|
||||
|
||||
// Garantir que as datas estão no formato correto e filtrar novamente para garantir
|
||||
registrosFiltrados = registros.filter((r) => {
|
||||
// Comparação de strings funciona para formato YYYY-MM-DD
|
||||
return r.data >= args.dataInicio && r.data <= args.dataFim;
|
||||
});
|
||||
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Registros após filtro:', registrosFiltrados.length);
|
||||
} catch (error) {
|
||||
console.error('[listarRegistrosPeriodo] Erro ao buscar registros:', error);
|
||||
// Fallback: buscar todos e filtrar manualmente
|
||||
const todosRegistros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.collect();
|
||||
|
||||
const todosRegistros = await ctx.db.query('registrosPonto').collect();
|
||||
|
||||
registrosFiltrados = todosRegistros.filter((r) => {
|
||||
return r.data >= args.dataInicio && r.data <= args.dataFim;
|
||||
});
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Fallback - registros encontrados:', registrosFiltrados.length);
|
||||
|
||||
console.log(
|
||||
'[listarRegistrosPeriodo] Fallback - registros encontrados:',
|
||||
registrosFiltrados.length
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Registros encontrados antes de buscar funcionários:', registrosFiltrados.length);
|
||||
console.log(
|
||||
'[listarRegistrosPeriodo] Registros encontrados antes de buscar funcionários:',
|
||||
registrosFiltrados.length
|
||||
);
|
||||
|
||||
// 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))
|
||||
);
|
||||
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
|
||||
@@ -977,13 +1005,16 @@ export const listarRegistrosPeriodo = query({
|
||||
q.eq('funcionarioId', funcId as Id<'funcionarios'>).eq('data', data)
|
||||
)
|
||||
.first();
|
||||
|
||||
|
||||
if (bancoHoras) {
|
||||
saldosPorDataFuncionario[chave] = bancoHoras.saldoMinutos;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Total de registros a retornar:', registrosFiltrados.length);
|
||||
console.log(
|
||||
'[listarRegistrosPeriodo] Total de registros a retornar:',
|
||||
registrosFiltrados.length
|
||||
);
|
||||
|
||||
return registrosFiltrados.map((registro) => {
|
||||
const funcionario = funcionarios.find((f) => f?._id === registro.funcionarioId);
|
||||
@@ -999,18 +1030,18 @@ export const listarRegistrosPeriodo = query({
|
||||
? {
|
||||
nome: funcionario.nome,
|
||||
matricula: funcionario.matricula,
|
||||
descricaoCargo: funcionario.descricaoCargo,
|
||||
descricaoCargo: funcionario.descricaoCargo
|
||||
}
|
||||
: null,
|
||||
saldoDiario: {
|
||||
saldoMinutos,
|
||||
horas,
|
||||
minutos,
|
||||
positivo,
|
||||
},
|
||||
positivo
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1020,7 +1051,7 @@ export const obterEstatisticas = query({
|
||||
args: {
|
||||
dataInicio: v.string(), // YYYY-MM-DD
|
||||
dataFim: v.string(), // YYYY-MM-DD
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
funcionarioId: v.optional(v.id('funcionarios'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1032,7 +1063,7 @@ export const obterEstatisticas = query({
|
||||
foraDoPrazo: 0,
|
||||
totalFuncionarios: 0,
|
||||
funcionariosDentroPrazo: 0,
|
||||
funcionariosForaPrazo: 0,
|
||||
funcionariosForaPrazo: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1070,9 +1101,9 @@ export const obterEstatisticas = query({
|
||||
foraDoPrazo,
|
||||
totalFuncionarios,
|
||||
funcionariosDentroPrazo,
|
||||
funcionariosForaPrazo,
|
||||
funcionariosForaPrazo
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1080,7 +1111,7 @@ export const obterEstatisticas = query({
|
||||
*/
|
||||
export const obterRegistro = query({
|
||||
args: {
|
||||
registroId: v.id('registrosPonto'),
|
||||
registroId: v.id('registrosPonto')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1122,13 +1153,13 @@ export const obterRegistro = query({
|
||||
simbolo: simbolo
|
||||
? {
|
||||
nome: simbolo.nome,
|
||||
tipo: simbolo.tipo,
|
||||
tipo: simbolo.tipo
|
||||
}
|
||||
: null,
|
||||
: null
|
||||
}
|
||||
: null,
|
||||
: null
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1142,7 +1173,9 @@ function calcularCargaHorariaDiaria(config: {
|
||||
}): 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 [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco
|
||||
.split(':')
|
||||
.map(Number);
|
||||
const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number);
|
||||
|
||||
const minutosEntrada = horaEntrada * 60 + minutoEntrada;
|
||||
@@ -1160,11 +1193,13 @@ function calcularCargaHorariaDiaria(config: {
|
||||
/**
|
||||
* Calcula horas trabalhadas do dia baseado nos registros
|
||||
*/
|
||||
function calcularHorasTrabalhadas(registros: Array<{
|
||||
tipo: string;
|
||||
hora: number;
|
||||
minuto: number;
|
||||
}>): number {
|
||||
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;
|
||||
@@ -1247,7 +1282,7 @@ async function atualizarBancoHoras(
|
||||
horasTrabalhadas,
|
||||
saldoMinutos,
|
||||
registrosPontoIds,
|
||||
calculadoEm: Date.now(),
|
||||
calculadoEm: Date.now()
|
||||
});
|
||||
} else {
|
||||
// Criar novo
|
||||
@@ -1258,7 +1293,7 @@ async function atualizarBancoHoras(
|
||||
horasTrabalhadas,
|
||||
saldoMinutos,
|
||||
registrosPontoIds,
|
||||
calculadoEm: Date.now(),
|
||||
calculadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1270,7 +1305,7 @@ export const obterHistoricoESaldoDia = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
data: v.string(), // YYYY-MM-DD
|
||||
_refresh: v.optional(v.number()), // Parâmetro usado pelo frontend para forçar refresh
|
||||
_refresh: v.optional(v.number()) // Parâmetro usado pelo frontend para forçar refresh
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1281,7 +1316,7 @@ export const obterHistoricoESaldoDia = query({
|
||||
registros: [],
|
||||
cargaHorariaDiaria: 0,
|
||||
horasTrabalhadas: 0,
|
||||
saldoMinutos: 0,
|
||||
saldoMinutos: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1298,7 +1333,7 @@ export const obterHistoricoESaldoDia = query({
|
||||
)
|
||||
.order('asc')
|
||||
.collect();
|
||||
|
||||
|
||||
console.log('[obterHistoricoESaldoDia] Registros encontrados:', registros.length, {
|
||||
funcionarioId: args.funcionarioId,
|
||||
data: args.data
|
||||
@@ -1315,7 +1350,7 @@ export const obterHistoricoESaldoDia = query({
|
||||
registros: [],
|
||||
cargaHorariaDiaria: 0,
|
||||
horasTrabalhadas: 0,
|
||||
saldoMinutos: 0,
|
||||
saldoMinutos: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1336,10 +1371,10 @@ export const obterHistoricoESaldoDia = query({
|
||||
saldoFormatado: {
|
||||
horas,
|
||||
minutos,
|
||||
positivo,
|
||||
},
|
||||
positivo
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1347,7 +1382,7 @@ export const obterHistoricoESaldoDia = query({
|
||||
*/
|
||||
export const obterBancoHorasFuncionario = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
funcionarioId: v.id('funcionarios')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1373,9 +1408,9 @@ export const obterBancoHorasFuncionario = query({
|
||||
return {
|
||||
bancosHoras,
|
||||
saldoAcumuladoMinutos,
|
||||
totalDias: bancosHoras.length,
|
||||
totalDias: bancosHoras.length
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1411,7 +1446,7 @@ export const editarRegistroPonto = mutation({
|
||||
motivoId: v.optional(v.string()),
|
||||
motivoTipo: v.optional(v.string()),
|
||||
motivoDescricao: v.optional(v.string()),
|
||||
observacoes: v.optional(v.string()),
|
||||
observacoes: v.optional(v.string())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1439,7 +1474,7 @@ export const editarRegistroPonto = mutation({
|
||||
await ctx.db.patch(args.registroId, {
|
||||
hora: args.horaNova,
|
||||
minuto: args.minutoNova,
|
||||
editadoPorGestor: true,
|
||||
editadoPorGestor: true
|
||||
});
|
||||
|
||||
// Criar registro de homologação
|
||||
@@ -1455,12 +1490,12 @@ export const editarRegistroPonto = mutation({
|
||||
motivoTipo: args.motivoTipo,
|
||||
motivoDescricao: args.motivoDescricao,
|
||||
observacoes: args.observacoes,
|
||||
criadoEm: Date.now(),
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
// Atualizar registro com ID da homologação
|
||||
await ctx.db.patch(args.registroId, {
|
||||
homologacaoId,
|
||||
homologacaoId
|
||||
});
|
||||
|
||||
// Recalcular banco de horas do dia
|
||||
@@ -1474,7 +1509,7 @@ export const editarRegistroPonto = mutation({
|
||||
}
|
||||
|
||||
return { success: true, homologacaoId };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1490,7 +1525,7 @@ export const ajustarBancoHoras = mutation({
|
||||
motivoId: v.optional(v.string()),
|
||||
motivoTipo: v.optional(v.string()),
|
||||
motivoDescricao: v.optional(v.string()),
|
||||
observacoes: v.optional(v.string()),
|
||||
observacoes: v.optional(v.string())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1505,8 +1540,7 @@ export const ajustarBancoHoras = mutation({
|
||||
}
|
||||
|
||||
// Calcular ajuste em minutos
|
||||
const ajusteMinutos =
|
||||
args.periodoDias * 24 * 60 + args.periodoHoras * 60 + args.periodoMinutos;
|
||||
const ajusteMinutos = args.periodoDias * 24 * 60 + args.periodoHoras * 60 + args.periodoMinutos;
|
||||
|
||||
// Aplicar sinal baseado no tipo de ajuste
|
||||
let ajusteFinal = ajusteMinutos;
|
||||
@@ -1526,7 +1560,7 @@ export const ajustarBancoHoras = mutation({
|
||||
if (bancoHorasAtual) {
|
||||
// Atualizar saldo do dia atual
|
||||
await ctx.db.patch(bancoHorasAtual._id, {
|
||||
saldoMinutos: bancoHorasAtual.saldoMinutos + ajusteFinal,
|
||||
saldoMinutos: bancoHorasAtual.saldoMinutos + ajusteFinal
|
||||
});
|
||||
} else {
|
||||
// Criar novo registro de banco de horas para o ajuste
|
||||
@@ -1548,7 +1582,7 @@ export const ajustarBancoHoras = mutation({
|
||||
horasTrabalhadas: 0,
|
||||
saldoMinutos: ajusteFinal,
|
||||
registrosPontoIds: [],
|
||||
calculadoEm: Date.now(),
|
||||
calculadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1565,11 +1599,11 @@ export const ajustarBancoHoras = mutation({
|
||||
periodoHoras: args.periodoHoras,
|
||||
periodoMinutos: args.periodoMinutos,
|
||||
ajusteMinutos: ajusteFinal,
|
||||
criadoEm: Date.now(),
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
return { success: true, homologacaoId, ajusteMinutos: ajusteFinal };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1577,7 +1611,7 @@ export const ajustarBancoHoras = mutation({
|
||||
*/
|
||||
export const listarHomologacoes = query({
|
||||
args: {
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
funcionarioId: v.optional(v.id('funcionarios'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1623,26 +1657,26 @@ export const listarHomologacoes = query({
|
||||
funcionario: funcionario
|
||||
? {
|
||||
nome: funcionario.nome,
|
||||
matricula: funcionario.matricula,
|
||||
matricula: funcionario.matricula
|
||||
}
|
||||
: null,
|
||||
gestor: gestor
|
||||
? {
|
||||
nome: gestor.nome,
|
||||
nome: gestor.nome
|
||||
}
|
||||
: null,
|
||||
registro: registro
|
||||
? {
|
||||
data: registro.data,
|
||||
tipo: registro.tipo,
|
||||
tipo: registro.tipo
|
||||
}
|
||||
: null,
|
||||
: null
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return homologacoesComDetalhes;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1650,7 +1684,7 @@ export const listarHomologacoes = query({
|
||||
*/
|
||||
export const excluirHomologacao = mutation({
|
||||
args: {
|
||||
homologacaoId: v.id('homologacoesPonto'),
|
||||
homologacaoId: v.id('homologacoesPonto')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1664,7 +1698,11 @@ export const excluirHomologacao = mutation({
|
||||
}
|
||||
|
||||
// Verificar se é gestor do funcionário
|
||||
const isGestor = await verificarGestorDoFuncionario(ctx, usuario._id, homologacao.funcionarioId);
|
||||
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');
|
||||
}
|
||||
@@ -1675,7 +1713,7 @@ export const excluirHomologacao = mutation({
|
||||
if (registro && registro.homologacaoId === args.homologacaoId) {
|
||||
await ctx.db.patch(homologacao.registroId, {
|
||||
homologacaoId: undefined,
|
||||
editadoPorGestor: false,
|
||||
editadoPorGestor: false
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1684,7 +1722,7 @@ export const excluirHomologacao = mutation({
|
||||
await ctx.db.delete(args.homologacaoId);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1710,10 +1748,10 @@ export const obterMotivosAtestados = query({
|
||||
'Ajuste Administrativo',
|
||||
'Compensação de Horas',
|
||||
'Abono',
|
||||
'Desconto em Folha',
|
||||
],
|
||||
'Desconto em Folha'
|
||||
]
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1729,7 +1767,7 @@ export const criarDispensaRegistro = mutation({
|
||||
horaFim: v.number(),
|
||||
minutoFim: v.number(),
|
||||
motivo: v.string(),
|
||||
isento: v.boolean(),
|
||||
isento: v.boolean()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1764,11 +1802,11 @@ export const criarDispensaRegistro = mutation({
|
||||
motivo: args.motivo,
|
||||
isento: args.isento,
|
||||
ativo: true,
|
||||
criadoEm: Date.now(),
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
return { success: true, dispensaId };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1776,7 +1814,7 @@ export const criarDispensaRegistro = mutation({
|
||||
*/
|
||||
export const removerDispensaRegistro = mutation({
|
||||
args: {
|
||||
dispensaId: v.id('dispensasRegistro'),
|
||||
dispensaId: v.id('dispensasRegistro')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1797,11 +1835,11 @@ export const removerDispensaRegistro = mutation({
|
||||
|
||||
// Desativar dispensa
|
||||
await ctx.db.patch(args.dispensaId, {
|
||||
ativo: false,
|
||||
ativo: false
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1810,7 +1848,7 @@ export const removerDispensaRegistro = mutation({
|
||||
export const listarDispensas = query({
|
||||
args: {
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
apenasAtivas: v.optional(v.boolean()),
|
||||
apenasAtivas: v.optional(v.boolean())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1877,21 +1915,21 @@ export const listarDispensas = query({
|
||||
funcionario: funcionario
|
||||
? {
|
||||
nome: funcionario.nome,
|
||||
matricula: funcionario.matricula,
|
||||
matricula: funcionario.matricula
|
||||
}
|
||||
: null,
|
||||
gestor: gestor
|
||||
? {
|
||||
nome: gestor.nome,
|
||||
nome: gestor.nome
|
||||
}
|
||||
: null,
|
||||
expirada,
|
||||
expirada
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return dispensasComDetalhes;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1902,7 +1940,7 @@ export const verificarDispensaAtiva = query({
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
data: v.string(), // YYYY-MM-DD
|
||||
hora: v.optional(v.number()),
|
||||
minuto: v.optional(v.number()),
|
||||
minuto: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const dispensas = await ctx.db
|
||||
@@ -1919,7 +1957,7 @@ export const verificarDispensaAtiva = query({
|
||||
return {
|
||||
dispensado: true,
|
||||
dispensa,
|
||||
motivo: 'Isento de registro (caso excepcional)',
|
||||
motivo: 'Isento de registro (caso excepcional)'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1945,7 +1983,7 @@ export const verificarDispensaAtiva = query({
|
||||
return {
|
||||
dispensado: true,
|
||||
dispensa,
|
||||
motivo: dispensa.motivo,
|
||||
motivo: dispensa.motivo
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@@ -1953,7 +1991,7 @@ export const verificarDispensaAtiva = query({
|
||||
return {
|
||||
dispensado: true,
|
||||
dispensa,
|
||||
motivo: dispensa.motivo,
|
||||
motivo: dispensa.motivo
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1962,8 +2000,7 @@ export const verificarDispensaAtiva = query({
|
||||
return {
|
||||
dispensado: false,
|
||||
dispensa: null,
|
||||
motivo: null,
|
||||
motivo: null
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user