feat: enhance point registration and location validation features

- Refactored the RegistroPonto component to improve the layout and user experience, including a new section for displaying standard hours.
- Updated RelogioSincronizado to include GMT offset adjustments for accurate time display.
- Introduced new location validation logic in the backend to ensure point registrations are within allowed geofenced areas.
- Enhanced the device information schema to capture additional GPS data, improving the reliability of location checks.
- Added new endpoints for managing allowed marking addresses, facilitating better control over where points can be registered.
This commit is contained in:
2025-11-21 05:12:27 -03:00
parent 3da364fb02
commit d6aaa15cf4
17 changed files with 4347 additions and 568 deletions

View File

@@ -0,0 +1,246 @@
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
import { getCurrentUserFunction } from './auth';
import type { Id } from './_generated/dataModel';
/**
* Lista todas as associações de endereços para um funcionário
*/
export const listarAssociacoesFuncionario = query({
args: {
funcionarioId: v.id('funcionarios'),
incluirInativos: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
let associacoes;
if (args.incluirInativos) {
associacoes = await ctx.db
.query('funcionarioEnderecosMarcacao')
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId))
.collect();
} else {
associacoes = await ctx.db
.query('funcionarioEnderecosMarcacao')
.withIndex('by_funcionario_ativo', (q) =>
q.eq('funcionarioId', args.funcionarioId).eq('ativo', true)
)
.collect();
}
// Buscar dados completos dos endereços
const associacoesCompletas = await Promise.all(
associacoes.map(async (associacao) => {
const endereco = await ctx.db.get(associacao.enderecoMarcacaoId);
if (!endereco) return null;
// Usar raio personalizado se disponível, senão usar o raio padrão do endereço
const raioMetros = associacao.raioMetrosPersonalizado ?? endereco.raioMetros;
return {
...associacao,
raioMetros, // Raio usado (personalizado ou padrão)
endereco: {
_id: endereco._id,
nome: endereco.nome,
descricao: endereco.descricao,
latitude: endereco.latitude,
longitude: endereco.longitude,
endereco: endereco.endereco,
cep: endereco.cep,
cidade: endereco.cidade,
estado: endereco.estado,
pais: endereco.pais,
raioMetros: endereco.raioMetros, // Raio padrão do endereço
tipo: endereco.tipo,
ativo: endereco.ativo,
},
};
})
);
return associacoesCompletas.filter(
(item): item is NonNullable<typeof item> => item !== null
);
},
});
/**
* Associa um endereço a um funcionário
*/
export const associarEnderecoFuncionario = mutation({
args: {
funcionarioId: v.id('funcionarios'),
enderecoMarcacaoId: v.id('enderecosMarcacao'),
raioMetrosPersonalizado: v.optional(v.number()),
dataInicio: v.optional(v.string()), // YYYY-MM-DD
dataFim: v.optional(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ões (apenas TI ou admin)
// Validar que o funcionário existe
const funcionario = await ctx.db.get(args.funcionarioId);
if (!funcionario) {
throw new Error('Funcionário não encontrado');
}
// Validar que o endereço existe e está ativo
const endereco = await ctx.db.get(args.enderecoMarcacaoId);
if (!endereco) {
throw new Error('Endereço não encontrado');
}
if (!endereco.ativo) {
throw new Error('Endereço não está ativo');
}
// Validar raio personalizado se fornecido
if (args.raioMetrosPersonalizado !== undefined) {
if (args.raioMetrosPersonalizado < 0 || args.raioMetrosPersonalizado > 50000) {
throw new Error('Raio deve estar entre 0 e 50000 metros');
}
}
// Validar datas se fornecidas
if (args.dataInicio && args.dataFim) {
if (args.dataInicio > args.dataFim) {
throw new Error('Data de início deve ser anterior à data de fim');
}
}
// Verificar se já existe uma associação ativa
const associacaoExistente = await ctx.db
.query('funcionarioEnderecosMarcacao')
.withIndex('by_funcionario_ativo', (q) =>
q.eq('funcionarioId', args.funcionarioId).eq('ativo', true)
)
.filter((q) => q.eq(q.field('enderecoMarcacaoId'), args.enderecoMarcacaoId))
.first();
if (associacaoExistente) {
// Atualizar associação existente
await ctx.db.patch(associacaoExistente._id, {
raioMetrosPersonalizado: args.raioMetrosPersonalizado,
dataInicio: args.dataInicio,
dataFim: args.dataFim,
ativo: true,
});
return { associacaoId: associacaoExistente._id, atualizado: true };
}
// Criar nova associação
const associacaoId = await ctx.db.insert('funcionarioEnderecosMarcacao', {
funcionarioId: args.funcionarioId,
enderecoMarcacaoId: args.enderecoMarcacaoId,
raioMetrosPersonalizado: args.raioMetrosPersonalizado,
dataInicio: args.dataInicio,
dataFim: args.dataFim,
ativo: true,
criadoPor: usuario._id as Id<'usuarios'>,
criadoEm: Date.now(),
});
return { associacaoId, atualizado: false };
},
});
/**
* Atualiza uma associação de endereço com funcionário
*/
export const atualizarAssociacao = mutation({
args: {
associacaoId: v.id('funcionarioEnderecosMarcacao'),
raioMetrosPersonalizado: v.optional(v.number()),
dataInicio: v.optional(v.string()),
dataFim: v.optional(v.string()),
ativo: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
// TODO: Verificar permissões (apenas TI ou admin)
const associacao = await ctx.db.get(args.associacaoId);
if (!associacao) {
throw new Error('Associação não encontrada');
}
const atualizacoes: {
raioMetrosPersonalizado?: number;
dataInicio?: string;
dataFim?: string;
ativo?: boolean;
} = {};
if (args.raioMetrosPersonalizado !== undefined) {
if (args.raioMetrosPersonalizado < 0 || args.raioMetrosPersonalizado > 50000) {
throw new Error('Raio deve estar entre 0 e 50000 metros');
}
atualizacoes.raioMetrosPersonalizado = args.raioMetrosPersonalizado;
}
if (args.dataInicio !== undefined) {
atualizacoes.dataInicio = args.dataInicio || undefined;
}
if (args.dataFim !== undefined) {
atualizacoes.dataFim = args.dataFim || undefined;
}
if (args.dataInicio && args.dataFim) {
if (args.dataInicio > args.dataFim) {
throw new Error('Data de início deve ser anterior à data de fim');
}
}
if (args.ativo !== undefined) {
atualizacoes.ativo = args.ativo;
}
await ctx.db.patch(args.associacaoId, atualizacoes);
return { sucesso: true };
},
});
/**
* Remove uma associação de endereço com funcionário (desativa)
*/
export const removerAssociacao = mutation({
args: {
associacaoId: v.id('funcionarioEnderecosMarcacao'),
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
// TODO: Verificar permissões (apenas TI ou admin)
const associacao = await ctx.db.get(args.associacaoId);
if (!associacao) {
throw new Error('Associação não encontrada');
}
// Desativar ao invés de deletar
await ctx.db.patch(args.associacaoId, {
ativo: false,
});
return { sucesso: true };
},
});