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:
246
packages/backend/convex/funcionarioEnderecos.ts
Normal file
246
packages/backend/convex/funcionarioEnderecos.ts
Normal 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 };
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user