diff --git a/apps/web/src/lib/components/ponto/RelogioSincronizado.svelte b/apps/web/src/lib/components/ponto/RelogioSincronizado.svelte index 181d24f..9f09cc5 100644 --- a/apps/web/src/lib/components/ponto/RelogioSincronizado.svelte +++ b/apps/web/src/lib/components/ponto/RelogioSincronizado.svelte @@ -9,12 +9,21 @@ let tempoAtual = $state(new Date()); let sincronizado = $state(false); + let sincronizando = $state(false); let usandoServidorExterno = $state(false); let offsetSegundos = $state(0); let erro = $state(null); let intervalId: ReturnType | null = null; + let intervaloSincronizacao: ReturnType | null = null; + let sincronizacaoEmAndamento = $state(false); // Flag para evitar múltiplas sincronizações simultâneas async function atualizarTempo() { + // Evitar múltiplas sincronizações simultâneas + if (sincronizacaoEmAndamento) { + return; + } + sincronizacaoEmAndamento = true; + sincronizando = true; try { const config = await client.query(api.configuracaoRelogio.obterConfiguracao, {}); // Usar gmtOffset da configuração, sem valor padrão, pois 0 é um valor válido @@ -25,7 +34,12 @@ if (config.usarServidorExterno) { try { - const resultado = await client.action(api.configuracaoRelogio.sincronizarTempo, {}); + // Adicionar timeout de 10 segundos para sincronização + const sincronizacaoPromise = client.action(api.configuracaoRelogio.sincronizarTempo, {}); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout na sincronização (10s)')), 10000) + ); + const resultado = await Promise.race([sincronizacaoPromise, timeoutPromise]); if (resultado.sucesso && resultado.timestamp) { timestampBase = resultado.timestamp; sincronizado = true; @@ -43,7 +57,11 @@ usandoServidorExterno = false; erro = 'Usando relógio do PC (falha na sincronização)'; } else { - throw error; + // Mesmo sem fallback configurado, usar PC como última opção + timestampBase = obterTempoPC(); + sincronizado = false; + usandoServidorExterno = false; + erro = 'Usando relógio do PC (servidor indisponível)'; } } } else { @@ -71,6 +89,9 @@ tempoAtual = new Date(obterTempoPC()); sincronizado = false; erro = 'Erro ao obter tempo do servidor'; + } finally { + sincronizando = false; + sincronizacaoEmAndamento = false; } } @@ -81,17 +102,34 @@ } onMount(async () => { - await atualizarTempo(); - // Sincronizar a cada 30 segundos - setInterval(atualizarTempo, 30000); + // Inicializar com relógio do PC imediatamente para não bloquear a interface + tempoAtual = new Date(obterTempoPC()); + sincronizado = false; + erro = 'Usando relógio do PC'; // Atualizar display a cada segundo intervalId = setInterval(atualizarRelogio, 1000); + // Sincronizar em background (não bloquear) após um pequeno delay para garantir que a UI está renderizada + setTimeout(() => { + atualizarTempo().catch((error) => { + console.error('Erro ao sincronizar tempo em background:', error); + }); + }, 100); + // Sincronizar a cada 30 segundos + intervaloSincronizacao = setInterval(() => { + atualizarTempo().catch((error) => { + console.error('Erro ao sincronizar tempo periódico:', error); + }); + }, 30000); }); onDestroy(() => { if (intervalId) { clearInterval(intervalId); } + if (intervaloSincronizacao) { + clearInterval(intervaloSincronizacao); + } + sincronizacaoEmAndamento = false; }); const horaFormatada = $derived.by(() => { @@ -131,13 +169,18 @@
- {#if sincronizado} + {#if sincronizando} + + Sincronizando com servidor... + {:else if sincronizado} {#if usandoServidorExterno} diff --git a/packages/backend/convex/pontos.ts b/packages/backend/convex/pontos.ts index 889342e..7b7f488 100644 --- a/packages/backend/convex/pontos.ts +++ b/packages/backend/convex/pontos.ts @@ -1848,7 +1848,14 @@ async function atualizarBancoHoras( // Atualizar banco de horas mensal const mes = data.substring(0, 7); // YYYY-MM - await calcularBancoHorasMensal(ctx, funcionarioId, mes); + + // Verificar se estamos editando um mês passado + const hoje = new Date(); + const mesAtual = `${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}`; + const estaEditandoMesPassado = mes < mesAtual; + + // Se estamos editando um mês passado, recalcular em cascata para atualizar meses seguintes + await calcularBancoHorasMensal(ctx, funcionarioId, mes, estaEditandoMesPassado); } /** @@ -1979,14 +1986,74 @@ export const obterBancoHorasFuncionario = query({ } }); +/** + * Recalcula meses seguintes em cascata quando um mês anterior é atualizado + * Isso garante que os saldos iniciais dos meses seguintes sejam atualizados corretamente + */ +async function recalcularMesesSeguintes( + ctx: MutationCtx, + funcionarioId: Id<'funcionarios'>, + mesAtualizado: string // YYYY-MM do mês que foi atualizado +): Promise { + const hoje = new Date(); + const mesAtual = `${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}`; + + // Se o mês atualizado já é o mês atual ou futuro, não precisa recalcular nada + if (mesAtualizado >= mesAtual) { + return; + } + + // Recalcular todos os meses do mês seguinte ao atualizado até o mês atual + // Calcular primeiro mês a recalcular (mês seguinte ao atualizado) + const [anoAtualizado, mesNumAtualizado] = mesAtualizado.split('-').map(Number); + let anoIter = anoAtualizado; + let mesNumIter = mesNumAtualizado + 1; + if (mesNumIter > 12) { + mesNumIter = 1; + anoIter += 1; + } + + // Continuar enquanto o mês iterado for menor ou igual ao mês atual + while (true) { + const mesIterStr = `${anoIter}-${String(mesNumIter).padStart(2, '0')}`; + + // Se passou do mês atual, parar + if (mesIterStr > mesAtual) { + break; + } + + // Verificar se existe registro mensal para este mês + const bancoMensalExistente = await ctx.db + .query('bancoHorasMensal') + .withIndex('by_funcionario_mes', (q) => + q.eq('funcionarioId', funcionarioId).eq('mes', mesIterStr) + ) + .first(); + + // Se existe registro, recalcular (o saldo inicial mudou porque o mês anterior mudou) + if (bancoMensalExistente) { + await calcularBancoHorasMensal(ctx, funcionarioId, mesIterStr, false); // false = não recalcular cascata novamente + } + + // Avançar para o próximo mês + mesNumIter += 1; + if (mesNumIter > 12) { + mesNumIter = 1; + anoIter += 1; + } + } +} + /** * Calcula e atualiza banco de horas mensal para um funcionário * Esta função deve ser chamada após atualizações no banco de horas diário + * @param recalcularCascata - Se true, recalcula automaticamente os meses seguintes (padrão: true) */ async function calcularBancoHorasMensal( ctx: MutationCtx, funcionarioId: Id<'funcionarios'>, - mes: string // YYYY-MM + mes: string, // YYYY-MM + recalcularCascata: boolean = true // Por padrão, recalcula em cascata ): Promise { // Buscar todos os bancoHoras do mês const dataInicio = `${mes}-01`; @@ -2106,6 +2173,11 @@ async function calcularBancoHorasMensal( atualizadoEm: agora }); } + + // Recalcular meses seguintes em cascata se solicitado + if (recalcularCascata) { + await recalcularMesesSeguintes(ctx, funcionarioId, mes); + } } /** @@ -2611,7 +2683,14 @@ export const ajustarBancoHoras = mutation({ // Recalcular banco de horas mensal após ajuste const mes = hoje.substring(0, 7); // YYYY-MM - await calcularBancoHorasMensal(ctx, args.funcionarioId, mes); + + // Verificar se estamos ajustando um mês passado + const hojeDate = new Date(); + const mesAtual = `${hojeDate.getFullYear()}-${String(hojeDate.getMonth() + 1).padStart(2, '0')}`; + const estaAjustandoMesPassado = mes < mesAtual; + + // Se estamos ajustando um mês passado, recalcular em cascata para atualizar meses seguintes + await calcularBancoHorasMensal(ctx, args.funcionarioId, mes, estaAjustandoMesPassado); // Criar registro de homologação (mantido para compatibilidade) const homologacaoId = await ctx.db.insert('homologacoesPonto', { @@ -3872,7 +3951,14 @@ export const criarAjusteBancoHoras = mutation({ // Recalcular banco de horas mensal const mes = args.dataAplicacao.substring(0, 7); - await calcularBancoHorasMensal(ctx, args.funcionarioId, mes); + + // Verificar se estamos aplicando ajuste em um mês passado + const hoje = new Date(); + const mesAtual = `${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}`; + const estaAplicandoEmMesPassado = mes < mesAtual; + + // Se estamos aplicando em um mês passado, recalcular em cascata para atualizar meses seguintes + await calcularBancoHorasMensal(ctx, args.funcionarioId, mes, estaAplicandoEmMesPassado); return { ajusteId, success: true }; }