feat: enhance point registration and management features

- Added functionality to capture and display images during point registration, improving user experience.
- Implemented error handling for image uploads and webcam access, ensuring smoother operation.
- Introduced a justification field for point registration, allowing users to provide context for their entries.
- Enhanced the backend to support new features, including image handling and justification storage.
- Updated UI components for better layout and responsiveness, improving overall usability.
This commit is contained in:
2025-11-18 15:28:26 -03:00
parent f0c6e4468f
commit b01d2d6786
10 changed files with 941 additions and 187 deletions

View File

@@ -1,5 +1,5 @@
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
import { internalMutation, mutation, query } from './_generated/server';
import type { MutationCtx, QueryCtx } from './_generated/server';
import { getCurrentUserFunction } from './auth';
import type { Id } from './_generated/dataModel';
@@ -52,6 +52,7 @@ interface InformacoesDispositivo {
/**
* 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,
@@ -59,6 +60,11 @@ function calcularStatusPonto(
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;
@@ -141,6 +147,7 @@ export const registrarPonto = mutation({
),
timestamp: v.number(),
sincronizadoComServidor: v.boolean(),
justificativa: v.optional(v.string()),
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
@@ -225,6 +232,7 @@ export const registrarPonto = mutation({
sincronizadoComServidor: args.sincronizadoComServidor,
toleranciaMinutos: config.toleranciaMinutos,
dentroDoPrazo,
justificativa: args.justificativa,
ipAddress: args.informacoesDispositivo?.ipAddress,
ipPublico: args.informacoesDispositivo?.ipPublico,
ipLocal: args.informacoesDispositivo?.ipLocal,
@@ -257,6 +265,9 @@ export const registrarPonto = mutation({
criadoEm: Date.now(),
});
// Atualizar banco de horas após registrar
await atualizarBancoHoras(ctx, usuario.funcionarioId, data, config);
return { registroId, tipo, dentroDoPrazo };
},
});
@@ -421,8 +432,15 @@ export const obterRegistro = query({
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,
@@ -440,3 +458,228 @@ export const obterRegistro = query({
},
});
/**
* 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,
};
},
});

View File

@@ -1387,6 +1387,9 @@ export default defineSchema({
connectionType: v.optional(v.string()),
memoryInfo: v.optional(v.string()),
// Justificativa opcional para o registro
justificativa: v.optional(v.string()),
criadoEm: v.number(),
})
.index("by_funcionario_data", ["funcionarioId", "data"])
@@ -1416,5 +1419,19 @@ export default defineSchema({
atualizadoPor: v.id("usuarios"),
atualizadoEm: v.number(),
})
.index("by_ativo", ["usarServidorExterno"])
.index("by_ativo", ["usarServidorExterno"]),
// Banco de Horas - Saldo diário de horas trabalhadas
bancoHoras: defineTable({
funcionarioId: v.id("funcionarios"),
data: v.string(), // YYYY-MM-DD
cargaHorariaDiaria: v.number(), // Horas esperadas do dia (em minutos)
horasTrabalhadas: v.number(), // Horas realmente trabalhadas (em minutos)
saldoMinutos: v.number(), // Saldo do dia (positivo = horas extras, negativo = déficit)
registrosPontoIds: v.array(v.id("registrosPonto")), // IDs dos registros do dia
calculadoEm: v.number(),
})
.index("by_funcionario_data", ["funcionarioId", "data"])
.index("by_funcionario", ["funcionarioId"])
.index("by_data", ["data"]),
});