Files
sgse-app/apps/web/src/lib/components/ponto/RelogioSincronizado.svelte

201 lines
6.8 KiB
Svelte

<script lang="ts">
import { api } from '@sgse-app/backend/convex/_generated/api';
import { useConvexClient } from 'convex-svelte';
import { AlertCircle, CheckCircle2, Clock } from 'lucide-svelte';
import { onDestroy, onMount } from 'svelte';
import { obterTempoPC, obterTempoServidor } from '$lib/utils/sincronizacaoTempo';
const client = useConvexClient();
let tempoAtual = $state<Date>(new Date());
let sincronizado = $state(false);
let sincronizando = $state(false);
let usandoServidorExterno = $state(false);
let offsetSegundos = $state(0);
let erro = $state<string | null>(null);
let intervalId: ReturnType<typeof setInterval> | null = null;
let intervaloSincronizacao: ReturnType<typeof setInterval> | 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
// Se não estiver configurado, usar null e tratar como 0
const gmtOffset = config.gmtOffset ?? 0;
let timestampBase: number;
if (config.usarServidorExterno) {
try {
// Adicionar timeout de 10 segundos para sincronização
const sincronizacaoPromise = client.action(api.configuracaoRelogio.sincronizarTempo, {});
const timeoutPromise = new Promise<never>((_, 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;
usandoServidorExterno = resultado.usandoServidorExterno || false;
offsetSegundos = resultado.offsetSegundos || 0;
erro = null;
} else {
throw new Error('Falha ao sincronizar');
}
} catch (error) {
console.warn('Erro ao sincronizar:', error);
if (config.fallbackParaPC) {
timestampBase = obterTempoPC();
sincronizado = false;
usandoServidorExterno = false;
erro = 'Usando relógio do PC (falha na sincronização)';
} else {
// 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 {
// Usar relógio do PC (sem sincronização com servidor)
timestampBase = obterTempoPC();
sincronizado = false;
usandoServidorExterno = false;
erro = 'Usando relógio do PC';
}
// Aplicar GMT offset ao timestamp UTC
// O offset é aplicado manualmente, então usamos UTC como base para evitar conversão dupla
let timestampAjustado: number;
if (gmtOffset !== 0) {
// Aplicar offset configurado ao timestamp UTC
timestampAjustado = timestampBase + gmtOffset * 60 * 60 * 1000;
} else {
// Quando GMT = 0, manter timestamp UTC puro
timestampAjustado = timestampBase;
}
// Armazenar o timestamp ajustado (não o Date, para evitar problemas de timezone)
tempoAtual = new Date(timestampAjustado);
} catch (error) {
console.error('Erro ao obter tempo:', error);
tempoAtual = new Date(obterTempoPC());
sincronizado = false;
erro = 'Erro ao obter tempo do servidor';
} finally {
sincronizando = false;
sincronizacaoEmAndamento = false;
}
}
function atualizarRelogio() {
// Atualizar segundo a segundo
const agora = new Date(tempoAtual.getTime() + 1000);
tempoAtual = agora;
}
onMount(async () => {
// 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(() => {
// Usar UTC como base pois já aplicamos o offset manualmente no timestamp
// Isso evita conversão dupla pelo navegador
return tempoAtual.toLocaleTimeString('pt-BR', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: 'UTC' // Usar UTC como base pois já aplicamos o offset manualmente
});
});
const dataFormatada = $derived.by(() => {
// Usar UTC como base pois já aplicamos o offset manualmente no timestamp
// Isso evita conversão dupla pelo navegador
return tempoAtual.toLocaleDateString('pt-BR', {
weekday: 'long',
day: '2-digit',
month: 'long',
year: 'numeric',
timeZone: 'UTC' // Usar UTC como base pois já aplicamos o offset manualmente
});
});
</script>
<div class="flex w-full flex-col items-center gap-4">
<!-- Hora -->
<div class="text-primary font-mono text-5xl font-black tracking-tight drop-shadow-sm">
{horaFormatada}
</div>
<!-- Data -->
<div class="text-base-content/80 text-base font-semibold capitalize">
{dataFormatada}
</div>
<!-- Status de Sincronização -->
<div
class="flex items-center gap-2 rounded-full px-4 py-2 {sincronizando
? 'bg-info/20 text-info border-info/30 border animate-pulse'
: sincronizado
? 'bg-success/20 text-success border-success/30 border'
: erro
? 'bg-warning/20 text-warning border-warning/30 border'
: 'bg-base-300/50 text-base-content/60 border-base-300 border'}"
>
{#if sincronizando}
<span class="loading loading-spinner loading-sm text-info"></span>
<span class="text-sm font-semibold">Sincronizando com servidor...</span>
{:else if sincronizado}
<CheckCircle2 class="h-4 w-4" strokeWidth={2.5} />
<span class="text-sm font-semibold">
{#if usandoServidorExterno}
Sincronizado com servidor NTP
{:else}
Sincronizado com servidor
{/if}
</span>
{:else if erro}
<AlertCircle class="h-4 w-4" strokeWidth={2.5} />
<span class="text-sm font-semibold">{erro}</span>
{:else}
<Clock class="h-4 w-4" strokeWidth={2.5} />
<span class="text-sm font-semibold">Usando relógio do PC</span>
{/if}
</div>
</div>