From 196ef9064387a8ba77076d38469f3d1ef0223a0d Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Thu, 11 Dec 2025 11:53:20 -0300 Subject: [PATCH 1/5] chore: add empty lines to improve code readability in fichaPontoPDF and error handling components --- apps/web/src/lib/utils/fichaPontoPDF.ts | 2 ++ apps/web/src/routes/(dashboard)/+error.svelte | 2 ++ apps/web/src/routes/+error.svelte | 2 ++ 3 files changed, 6 insertions(+) diff --git a/apps/web/src/lib/utils/fichaPontoPDF.ts b/apps/web/src/lib/utils/fichaPontoPDF.ts index 77b62c4..9f98f87 100644 --- a/apps/web/src/lib/utils/fichaPontoPDF.ts +++ b/apps/web/src/lib/utils/fichaPontoPDF.ts @@ -444,3 +444,5 @@ export function adicionarRodape(doc: jsPDF): void { + + diff --git a/apps/web/src/routes/(dashboard)/+error.svelte b/apps/web/src/routes/(dashboard)/+error.svelte index 5b7d9a1..1c780c7 100644 --- a/apps/web/src/routes/(dashboard)/+error.svelte +++ b/apps/web/src/routes/(dashboard)/+error.svelte @@ -83,3 +83,5 @@ + + diff --git a/apps/web/src/routes/+error.svelte b/apps/web/src/routes/+error.svelte index 6dcb403..ae3a9ab 100644 --- a/apps/web/src/routes/+error.svelte +++ b/apps/web/src/routes/+error.svelte @@ -83,3 +83,5 @@ + + -- 2.49.1 From 6936a59c21d9ead2b64f8151ddd1bdecec3d7ea0 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Thu, 11 Dec 2025 16:52:07 -0300 Subject: [PATCH 2/5] feat: implement cascading recalculation of monthly hour banks when past months are updated or adjusted --- packages/backend/convex/pontos.ts | 94 +++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/packages/backend/convex/pontos.ts b/packages/backend/convex/pontos.ts index a0685d1..65bd06b 100644 --- a/packages/backend/convex/pontos.ts +++ b/packages/backend/convex/pontos.ts @@ -1800,7 +1800,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); } /** @@ -1918,14 +1925,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`; @@ -2045,6 +2112,11 @@ async function calcularBancoHorasMensal( atualizadoEm: agora }); } + + // Recalcular meses seguintes em cascata se solicitado + if (recalcularCascata) { + await recalcularMesesSeguintes(ctx, funcionarioId, mes); + } } /** @@ -2534,7 +2606,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', { @@ -3727,7 +3806,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 }; } -- 2.49.1 From 457e89e38620764426f730a478f4cc5c21e0f29e Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Fri, 12 Dec 2025 11:13:56 -0300 Subject: [PATCH 3/5] feat: enhance time synchronization logic with timeout and loading state management --- .../ponto/RelogioSincronizado.svelte | 65 +++++++++++++++---- 1 file changed, 54 insertions(+), 11 deletions(-) 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} -- 2.49.1 From 4faf279c3e396d480362d9f58b050cda1d9f1a40 Mon Sep 17 00:00:00 2001 From: killer-cf Date: Sat, 13 Dec 2025 20:05:27 -0300 Subject: [PATCH 4/5] feat: refine login page functionality by adding validation, improving error handling, and enhancing user feedback mechanisms --- apps/web/src/lib/components/ui/Button.svelte | 128 +++++++++ apps/web/src/lib/components/ui/Input.svelte | 174 +++++++++++ apps/web/src/lib/components/ui/Select.svelte | 287 +++++++++++++++++++ 3 files changed, 589 insertions(+) create mode 100644 apps/web/src/lib/components/ui/Button.svelte create mode 100644 apps/web/src/lib/components/ui/Input.svelte create mode 100644 apps/web/src/lib/components/ui/Select.svelte diff --git a/apps/web/src/lib/components/ui/Button.svelte b/apps/web/src/lib/components/ui/Button.svelte new file mode 100644 index 0000000..e7ab5d8 --- /dev/null +++ b/apps/web/src/lib/components/ui/Button.svelte @@ -0,0 +1,128 @@ + + + diff --git a/apps/web/src/lib/components/ui/Input.svelte b/apps/web/src/lib/components/ui/Input.svelte new file mode 100644 index 0000000..f170a9d --- /dev/null +++ b/apps/web/src/lib/components/ui/Input.svelte @@ -0,0 +1,174 @@ + + + +
+ + {label} + + {@render right?.()} +
+ +
+ {#if left} +
+
+ {@render left()} +
+
+ {/if} + + + + {#if right} +
+
+ {@render right()} +
+
+ {/if} +
+ + {#if helperText && !error} + + {helperText} + + {/if} + + {#if error} + + {error} + + {/if} +
diff --git a/apps/web/src/lib/components/ui/Select.svelte b/apps/web/src/lib/components/ui/Select.svelte new file mode 100644 index 0000000..d94f806 --- /dev/null +++ b/apps/web/src/lib/components/ui/Select.svelte @@ -0,0 +1,287 @@ + + + +
+ + {label} + + {@render labelRight?.()} +
+ + + + +
+ {#if triggerLeft} + + {@render triggerLeft()} + + {/if} + +
+ +
+ {#if canClear} + + + + {/if} + + + +
+
+
+ + + + +
+ {#if hasGroups} + {#each groups as [groupLabel, groupItems] (groupLabel)} + + + {groupLabel} + + {#each groupItems as item (item.value)} + + {item.label} + + + + + {/each} + + {/each} + {:else} + {#each items as item (item.value)} + + {item.label} + + + + + {/each} + {/if} +
+
+
+
+ + +
+ + {#if helperText && !error} + + {helperText} + + {/if} + + {#if error} + + {error} + + {/if} +
-- 2.49.1 From c272ca05e88ac0b6f3003011fc49ebd3a87fa33e Mon Sep 17 00:00:00 2001 From: killer-cf Date: Mon, 15 Dec 2025 09:01:56 -0300 Subject: [PATCH 5/5] feat: implement theme persistence and selection in header component, enhancing user experience with localStorage integration --- apps/web/src/lib/components/Header.svelte | 41 +++++++++++++++++ .../dashboard/DashboardHeaderActions.svelte | 3 -- apps/web/src/lib/utils/temas.ts | 46 +++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/apps/web/src/lib/components/Header.svelte b/apps/web/src/lib/components/Header.svelte index e5e7e3e..e9dece8 100644 --- a/apps/web/src/lib/components/Header.svelte +++ b/apps/web/src/lib/components/Header.svelte @@ -2,6 +2,8 @@ import { resolve } from '$app/paths'; import logo from '$lib/assets/logo_governo_PE.png'; import type { Snippet } from 'svelte'; + import { onMount } from 'svelte'; + import { aplicarTemaDaisyUI } from '$lib/utils/temas'; type HeaderProps = { left?: Snippet; @@ -9,6 +11,43 @@ }; const { left, right }: HeaderProps = $props(); + + let themeSelectEl: HTMLSelectElement | null = null; + + function safeGetThemeLS(): string | null { + try { + const t = localStorage.getItem('theme'); + return t && t.trim() ? t : null; + } catch { + return null; + } + } + + onMount(() => { + const persisted = safeGetThemeLS(); + if (persisted) { + // Sincroniza UI + HTML com o valor persistido (evita select ficar "aqua" indevido) + if (themeSelectEl && themeSelectEl.value !== persisted) { + themeSelectEl.value = persisted; + } + aplicarTemaDaisyUI(persisted); + } + }); + + function onThemeChange(e: Event) { + const nextValue = (e.currentTarget as HTMLSelectElement | null)?.value ?? null; + + // Se o theme-change não atualizar (caso comum após login/logout), + // garantimos aqui a persistência + aplicação imediata. + if (nextValue) { + try { + localStorage.setItem('theme', nextValue); + } catch { + // ignore + } + aplicarTemaDaisyUI(nextValue); + } + }