import { v } from 'convex/values'; import { mutation, query } from './_generated/server'; import { getCurrentUserFunction } from './auth'; import type { Id } from './_generated/dataModel'; /** * Valida formato de horário HH:mm */ function validarHorario(horario: string): boolean { const regex = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/; return regex.test(horario); } /** * Obtém a configuração ativa de ponto */ export const obterConfiguracao = query({ args: {}, handler: async (ctx) => { const config = await ctx.db .query('configuracaoPonto') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .first(); if (!config) { // Retornar configuração padrão se não houver return { horarioEntrada: '08:00', horarioSaidaAlmoco: '12:00', horarioRetornoAlmoco: '13:00', horarioSaida: '17:00', toleranciaMinutos: 15, ativo: false, }; } return config; }, }); /** * Salva configuração de ponto (apenas TI) */ export const salvarConfiguracao = mutation({ args: { horarioEntrada: v.string(), horarioSaidaAlmoco: v.string(), horarioRetornoAlmoco: v.string(), horarioSaida: v.string(), toleranciaMinutos: v.number(), }, handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } // TODO: Verificar se usuário tem permissão de TI // Por enquanto, permitir se tiver roleId // Validar horários if (!validarHorario(args.horarioEntrada)) { throw new Error('Horário de entrada inválido (formato: HH:mm)'); } if (!validarHorario(args.horarioSaidaAlmoco)) { throw new Error('Horário de saída para almoço inválido (formato: HH:mm)'); } if (!validarHorario(args.horarioRetornoAlmoco)) { throw new Error('Horário de retorno do almoço inválido (formato: HH:mm)'); } if (!validarHorario(args.horarioSaida)) { throw new Error('Horário de saída inválido (formato: HH:mm)'); } // Validar tolerância if (args.toleranciaMinutos < 0 || args.toleranciaMinutos > 60) { throw new Error('Tolerância deve estar entre 0 e 60 minutos'); } // Validar sequência lógica de horários const [horaEntrada, minutoEntrada] = args.horarioEntrada.split(':').map(Number); const [horaSaidaAlmoco, minutoSaidaAlmoco] = args.horarioSaidaAlmoco.split(':').map(Number); const [horaRetornoAlmoco, minutoRetornoAlmoco] = args.horarioRetornoAlmoco.split(':').map(Number); const [horaSaida, minutoSaida] = args.horarioSaida.split(':').map(Number); const minutosEntrada = horaEntrada * 60 + minutoEntrada; const minutosSaidaAlmoco = horaSaidaAlmoco * 60 + minutoSaidaAlmoco; const minutosRetornoAlmoco = horaRetornoAlmoco * 60 + minutoRetornoAlmoco; const minutosSaida = horaSaida * 60 + minutoSaida; if (minutosEntrada >= minutosSaidaAlmoco) { throw new Error('Horário de entrada deve ser anterior à saída para almoço'); } if (minutosSaidaAlmoco >= minutosRetornoAlmoco) { throw new Error('Horário de saída para almoço deve ser anterior ao retorno'); } if (minutosRetornoAlmoco >= minutosSaida) { throw new Error('Horário de retorno do almoço deve ser anterior à saída'); } // Desativar configurações antigas const configsAntigas = await ctx.db .query('configuracaoPonto') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .collect(); for (const configAntiga of configsAntigas) { await ctx.db.patch(configAntiga._id, { ativo: false }); } // Criar nova configuração const configId = await ctx.db.insert('configuracaoPonto', { horarioEntrada: args.horarioEntrada, horarioSaidaAlmoco: args.horarioSaidaAlmoco, horarioRetornoAlmoco: args.horarioRetornoAlmoco, horarioSaida: args.horarioSaida, toleranciaMinutos: args.toleranciaMinutos, ativo: true, atualizadoPor: usuario._id as Id<'usuarios'>, atualizadoEm: Date.now(), }); return { configId }; }, });