feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.

This commit is contained in:
2025-12-02 16:37:48 -03:00
parent 05e7f1181d
commit 4bd9e21748
265 changed files with 29156 additions and 26460 deletions

View File

@@ -1,19 +1,14 @@
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
import { getCurrentUserFunction } from './auth';
import type { Id } from './_generated/dataModel';
import type { MutationCtx } from './_generated/server';
import { mutation, query } from './_generated/server';
import { getCurrentUserFunction } from './auth';
/**
* 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;
@@ -32,7 +27,7 @@ function calcularDistancia(
*/
export const listarEnderecos = query({
args: {
incluirInativos: v.optional(v.boolean()),
incluirInativos: v.optional(v.boolean())
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
@@ -52,7 +47,7 @@ export const listarEnderecos = query({
// Ordenar por nome
return enderecos.sort((a, b) => a.nome.localeCompare(b.nome));
},
}
});
/**
@@ -60,7 +55,7 @@ export const listarEnderecos = query({
*/
export const obterEndereco = query({
args: {
enderecoId: v.id('enderecosMarcacao'),
enderecoId: v.id('enderecosMarcacao')
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
@@ -74,7 +69,7 @@ export const obterEndereco = query({
}
return endereco;
},
}
});
/**
@@ -98,7 +93,7 @@ export const criarEndereco = mutation({
v.literal('home_office'),
v.literal('deslocamento'),
v.literal('cliente')
),
)
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
@@ -155,11 +150,11 @@ export const criarEndereco = mutation({
tipo: args.tipo,
ativo: true,
criadoPor: usuario._id as Id<'usuarios'>,
criadoEm: Date.now(),
criadoEm: Date.now()
});
return { enderecoId };
},
}
});
/**
@@ -187,7 +182,7 @@ export const atualizarEndereco = mutation({
v.literal('deslocamento'),
v.literal('cliente')
)
),
)
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
@@ -235,14 +230,7 @@ export const atualizarEndereco = mutation({
const lat = args.latitude ?? endereco.latitude;
const lon = args.longitude ?? endereco.longitude;
if (
isNaN(lat) ||
lat < -90 ||
lat > 90 ||
isNaN(lon) ||
lon < -180 ||
lon > 180
) {
if (isNaN(lat) || lat < -90 || lat > 90 || isNaN(lon) || lon < -180 || lon > 180) {
throw new Error('Coordenadas inválidas');
}
@@ -304,7 +292,7 @@ export const atualizarEndereco = mutation({
await ctx.db.patch(args.enderecoId, atualizacoes);
return { sucesso: true };
},
}
});
/**
@@ -312,7 +300,7 @@ export const atualizarEndereco = mutation({
*/
export const desativarEndereco = mutation({
args: {
enderecoId: v.id('enderecosMarcacao'),
enderecoId: v.id('enderecosMarcacao')
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
@@ -330,11 +318,11 @@ export const desativarEndereco = mutation({
await ctx.db.patch(args.enderecoId, {
ativo: false,
atualizadoPor: usuario._id as Id<'usuarios'>,
atualizadoEm: Date.now(),
atualizadoEm: Date.now()
});
return { sucesso: true };
},
}
});
/**
@@ -344,7 +332,7 @@ export const desativarEndereco = mutation({
export const obterEnderecosFuncionario = query({
args: {
funcionarioId: v.id('funcionarios'),
dataAtual: v.optional(v.string()), // YYYY-MM-DD para validar períodos
dataAtual: v.optional(v.string()) // YYYY-MM-DD para validar períodos
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
@@ -387,13 +375,12 @@ export const obterEnderecosFuncionario = query({
if (!endereco || !endereco.ativo) continue;
// Usar raio personalizado se disponível, senão usar o raio do endereço
const raioMetros =
associacao.raioMetrosPersonalizado ?? endereco.raioMetros;
const raioMetros = associacao.raioMetrosPersonalizado ?? endereco.raioMetros;
enderecosPermitidos.push({
enderecoId: endereco._id,
raioMetros,
periodoValido: true,
periodoValido: true
});
}
@@ -409,7 +396,7 @@ export const obterEnderecosFuncionario = query({
enderecosPermitidos.push({
enderecoId: endereco._id,
raioMetros: endereco.raioMetros,
periodoValido: true,
periodoValido: true
});
}
}
@@ -423,7 +410,7 @@ export const obterEnderecosFuncionario = query({
return {
...endereco,
raioMetros: item.raioMetros,
periodoValido: item.periodoValido,
periodoValido: item.periodoValido
};
})
);
@@ -431,7 +418,7 @@ export const obterEnderecosFuncionario = query({
return enderecosCompletos.filter(
(endereco): endereco is NonNullable<typeof endereco> => endereco !== null
);
},
}
});
/**
@@ -465,7 +452,7 @@ async function validarLocalizacaoGeofencingInternal(
) {
return {
dentroRaio: false,
avisos: ['Coordenadas inválidas'],
avisos: ['Coordenadas inválidas']
};
}
@@ -500,12 +487,11 @@ async function validarLocalizacaoGeofencingInternal(
const endereco = await ctx.db.get(associacao.enderecoMarcacaoId);
if (!endereco || !endereco.ativo) continue;
const raioMetros =
associacao.raioMetrosPersonalizado ?? endereco.raioMetros;
const raioMetros = associacao.raioMetrosPersonalizado ?? endereco.raioMetros;
enderecosParaValidar.push({
enderecoId: endereco._id,
raioMetros,
raioMetros
});
}
@@ -520,39 +506,32 @@ async function validarLocalizacaoGeofencingInternal(
for (const endereco of enderecosSede) {
enderecosParaValidar.push({
enderecoId: endereco._id,
raioMetros: endereco.raioMetros,
raioMetros: endereco.raioMetros
});
}
}
// Se ainda não houver endereços, usar raio padrão
if (enderecosParaValidar.length === 0) {
avisos.push(
'Nenhum endereço de marcação configurado. Usando validação padrão.'
);
avisos.push('Nenhum endereço de marcação configurado. Usando validação padrão.');
return {
dentroRaio: true, // Não bloquear se não houver configuração
raioUsado: raioPadrao,
avisos,
avisos
};
}
// Calcular distância para cada endereço e encontrar o mais próximo
let enderecoMaisProximo: Id<'enderecosMarcacao'> | undefined = undefined;
let distanciaMinima: number | undefined = undefined;
let raioUsado: number | undefined = undefined;
let enderecoEncontrado: string | undefined = undefined;
let enderecoMaisProximo: Id<'enderecosMarcacao'> | undefined;
let distanciaMinima: number | undefined;
let raioUsado: number | undefined;
let enderecoEncontrado: string | undefined;
for (const item of enderecosParaValidar) {
const endereco = await ctx.db.get(item.enderecoId);
if (!endereco) continue;
const distancia = calcularDistancia(
latitude,
longitude,
endereco.latitude,
endereco.longitude
);
const distancia = calcularDistancia(latitude, longitude, endereco.latitude, endereco.longitude);
if (distanciaMinima === undefined || distancia < distanciaMinima) {
distanciaMinima = distancia;
@@ -565,7 +544,7 @@ async function validarLocalizacaoGeofencingInternal(
if (enderecoMaisProximo === undefined || distanciaMinima === undefined) {
return {
dentroRaio: false,
avisos: ['Não foi possível validar localização'],
avisos: ['Não foi possível validar localização']
};
}
@@ -585,7 +564,7 @@ async function validarLocalizacaoGeofencingInternal(
distanciaMetros: distanciaMinima,
raioUsado: raioUsado ?? raioPadrao,
enderecoEncontrado,
avisos,
avisos
};
}
@@ -598,7 +577,7 @@ export const validarLocalizacaoGeofencing = mutation({
funcionarioId: v.id('funcionarios'),
latitude: v.number(),
longitude: v.number(),
raioPadrao: v.optional(v.number()), // metros - usado se não houver endereços configurados
raioPadrao: v.optional(v.number()) // metros - usado se não houver endereços configurados
},
returns: v.object({
dentroRaio: v.boolean(),
@@ -606,7 +585,7 @@ export const validarLocalizacaoGeofencing = mutation({
distanciaMetros: v.optional(v.number()),
raioUsado: v.optional(v.number()),
enderecoEncontrado: v.optional(v.string()),
avisos: v.array(v.string()),
avisos: v.array(v.string())
}),
handler: async (ctx, args) => {
return await validarLocalizacaoGeofencingInternal(
@@ -616,12 +595,10 @@ export const validarLocalizacaoGeofencing = mutation({
args.longitude,
args.raioPadrao || 100
);
},
}
});
/**
* Exporta a função auxiliar para uso interno em outras mutations
*/
export { validarLocalizacaoGeofencingInternal };