- Refactored ErrorModal to use a div-based layout with enhanced animations and accessibility features. - Updated RegistroPonto to include a new loading state and improved modal handling for webcam capture. - Enhanced styling for better visual consistency and user experience across modals and registration cards. - Introduced comparative balance calculations in RegistroPonto for better visibility of time discrepancies.
154 lines
4.6 KiB
Svelte
154 lines
4.6 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { useConvexClient } from 'convex-svelte';
|
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
import { obterTempoServidor, obterTempoPC } from '$lib/utils/sincronizacaoTempo';
|
|
import { CheckCircle2, AlertCircle, Clock } from 'lucide-svelte';
|
|
|
|
const client = useConvexClient();
|
|
|
|
let tempoAtual = $state<Date>(new Date());
|
|
let sincronizado = $state(false);
|
|
let usandoServidorExterno = $state(false);
|
|
let offsetSegundos = $state(0);
|
|
let erro = $state<string | null>(null);
|
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
|
|
async function atualizarTempo() {
|
|
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 {
|
|
const resultado = await client.action(api.configuracaoRelogio.sincronizarTempo, {});
|
|
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 {
|
|
throw error;
|
|
}
|
|
}
|
|
} 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
|
|
// Quando GMT é 0, usar timestamp UTC puro e deixar toLocaleTimeString() fazer a conversão automática
|
|
// Quando GMT ≠ 0, aplicar offset configurado ao timestamp
|
|
let timestampAjustado: number;
|
|
if (gmtOffset !== 0) {
|
|
// Aplicar offset configurado
|
|
timestampAjustado = timestampBase + (gmtOffset * 60 * 60 * 1000);
|
|
} else {
|
|
// Quando GMT = 0, manter timestamp UTC puro
|
|
// O toLocaleTimeString() converterá automaticamente para o timezone local do navegador
|
|
timestampAjustado = timestampBase;
|
|
}
|
|
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';
|
|
}
|
|
}
|
|
|
|
function atualizarRelogio() {
|
|
// Atualizar segundo a segundo
|
|
const agora = new Date(tempoAtual.getTime() + 1000);
|
|
tempoAtual = agora;
|
|
}
|
|
|
|
onMount(async () => {
|
|
await atualizarTempo();
|
|
// Sincronizar a cada 30 segundos
|
|
setInterval(atualizarTempo, 30000);
|
|
// Atualizar display a cada segundo
|
|
intervalId = setInterval(atualizarRelogio, 1000);
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (intervalId) {
|
|
clearInterval(intervalId);
|
|
}
|
|
});
|
|
|
|
const horaFormatada = $derived.by(() => {
|
|
return tempoAtual.toLocaleTimeString('pt-BR', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
});
|
|
});
|
|
|
|
const dataFormatada = $derived.by(() => {
|
|
return tempoAtual.toLocaleDateString('pt-BR', {
|
|
weekday: 'long',
|
|
day: '2-digit',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<div class="flex flex-col items-center gap-4 w-full">
|
|
<!-- Hora -->
|
|
<div class="text-5xl font-black font-mono text-primary tracking-tight drop-shadow-sm">
|
|
{horaFormatada}
|
|
</div>
|
|
|
|
<!-- Data -->
|
|
<div class="text-base font-semibold text-base-content/80 capitalize">
|
|
{dataFormatada}
|
|
</div>
|
|
|
|
<!-- Status de Sincronização -->
|
|
<div class="flex items-center gap-2 px-4 py-2 rounded-full {
|
|
sincronizado
|
|
? 'bg-success/20 text-success border border-success/30'
|
|
: erro
|
|
? 'bg-warning/20 text-warning border border-warning/30'
|
|
: 'bg-base-300/50 text-base-content/60 border border-base-300'
|
|
}">
|
|
{#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>
|
|
|