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:
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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"]),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user