feat: enhance chat widget and point registration components with improved styling and synchronization handling, including border-radius adjustments and synchronization status messaging

This commit is contained in:
2025-12-22 16:04:03 -03:00
parent e03b6d7a65
commit a8a7469812
3 changed files with 79 additions and 52 deletions

View File

@@ -1315,10 +1315,9 @@
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pela borda superior" aria-label="Redimensionar janela pela borda superior"
class="absolute top-0 right-0 left-0 z-50 h-2 cursor-ns-resize transition-colors" class="absolute top-0 right-0 left-0 z-50 h-2 cursor-ns-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}" style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 24px 24px 0 0;"
onmousedown={(e) => handleResizeStart(e, 'n')} onmousedown={(e) => handleResizeStart(e, 'n')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'n')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'n')}
style="border-radius: 24px 24px 0 0;"
></div> ></div>
<!-- Bottom --> <!-- Bottom -->
<div <div
@@ -1326,10 +1325,9 @@
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pela borda inferior" aria-label="Redimensionar janela pela borda inferior"
class="absolute right-0 bottom-0 left-0 z-50 h-2 cursor-ns-resize transition-colors" class="absolute right-0 bottom-0 left-0 z-50 h-2 cursor-ns-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}" style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 0 0 24px 24px;"
onmousedown={(e) => handleResizeStart(e, 's')} onmousedown={(e) => handleResizeStart(e, 's')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 's')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 's')}
style="border-radius: 0 0 24px 24px;"
></div> ></div>
<!-- Left --> <!-- Left -->
<div <div
@@ -1337,10 +1335,9 @@
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pela borda esquerda" aria-label="Redimensionar janela pela borda esquerda"
class="absolute top-0 bottom-0 left-0 z-50 w-2 cursor-ew-resize transition-colors" class="absolute top-0 bottom-0 left-0 z-50 w-2 cursor-ew-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}" style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 24px 0 0 24px;"
onmousedown={(e) => handleResizeStart(e, 'w')} onmousedown={(e) => handleResizeStart(e, 'w')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'w')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'w')}
style="border-radius: 24px 0 0 24px;"
></div> ></div>
<!-- Right --> <!-- Right -->
<div <div
@@ -1348,10 +1345,9 @@
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pela borda direita" aria-label="Redimensionar janela pela borda direita"
class="absolute top-0 right-0 bottom-0 z-50 w-2 cursor-ew-resize transition-colors" class="absolute top-0 right-0 bottom-0 z-50 w-2 cursor-ew-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}" style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 0 24px 24px 0;"
onmousedown={(e) => handleResizeStart(e, 'e')} onmousedown={(e) => handleResizeStart(e, 'e')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'e')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'e')}
style="border-radius: 0 24px 24px 0;"
></div> ></div>
<!-- Corners --> <!-- Corners -->
<div <div
@@ -1359,40 +1355,36 @@
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pelo canto superior esquerdo" aria-label="Redimensionar janela pelo canto superior esquerdo"
class="absolute top-0 left-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors" class="absolute top-0 left-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}" style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 24px 0 0 0;"
onmousedown={(e) => handleResizeStart(e, 'nw')} onmousedown={(e) => handleResizeStart(e, 'nw')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'nw')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'nw')}
style="border-radius: 24px 0 0 0;"
></div> ></div>
<div <div
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pelo canto superior direito" aria-label="Redimensionar janela pelo canto superior direito"
class="absolute top-0 right-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors" class="absolute top-0 right-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}" style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 0 24px 0 0;"
onmousedown={(e) => handleResizeStart(e, 'ne')} onmousedown={(e) => handleResizeStart(e, 'ne')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'ne')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'ne')}
style="border-radius: 0 24px 0 0;"
></div> ></div>
<div <div
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pelo canto inferior esquerdo" aria-label="Redimensionar janela pelo canto inferior esquerdo"
class="absolute bottom-0 left-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors" class="absolute bottom-0 left-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}" style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 0 0 0 24px;"
onmousedown={(e) => handleResizeStart(e, 'sw')} onmousedown={(e) => handleResizeStart(e, 'sw')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'sw')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'sw')}
style="border-radius: 0 0 0 24px;"
></div> ></div>
<div <div
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pelo canto inferior direito" aria-label="Redimensionar janela pelo canto inferior direito"
class="absolute right-0 bottom-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors" class="absolute right-0 bottom-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}" style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 0 0 24px 0;"
onmousedown={(e) => handleResizeStart(e, 'se')} onmousedown={(e) => handleResizeStart(e, 'se')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'se')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'se')}
style="border-radius: 0 0 24px 0;"
></div> ></div>
</div> </div>
</div> </div>

View File

@@ -35,6 +35,9 @@
const client = useConvexClient(); const client = useConvexClient();
// Estado de sincronização do relógio
let sincronizacaoConcluida = $state(false);
// Chave de refresh para forçar atualização das queries após registro // Chave de refresh para forçar atualização das queries após registro
let refreshKey = $state(0); let refreshKey = $state(0);
@@ -60,17 +63,12 @@
funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip' funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip'
); );
const registrosHojeQuery = $derived.by(() => // Queries de ponto - usando useQuery com parâmetros derivados reativos
useQuery(api.pontos.listarRegistrosDia, registrosHojeParams) const registrosHojeQuery = useQuery(api.pontos.listarRegistrosDia, registrosHojeParams);
);
const historicoSaldoQuery = $derived.by(() => const historicoSaldoQuery = useQuery(api.pontos.obterHistoricoESaldoDia, historicoSaldoParams);
useQuery(api.pontos.obterHistoricoESaldoDia, historicoSaldoParams)
);
const dispensaQuery = $derived.by(() => const dispensaQuery = useQuery(api.pontos.verificarDispensaAtiva, dispensaParams);
useQuery(api.pontos.verificarDispensaAtiva, dispensaParams)
);
// Query para obter status atual do funcionário (férias/licença) // Query para obter status atual do funcionário (férias/licença)
const funcionarioStatusQuery = useQuery( const funcionarioStatusQuery = useQuery(
@@ -355,6 +353,9 @@
justificativa = ''; // Limpar justificativa após registro justificativa = ''; // Limpar justificativa após registro
mostrandoModalConfirmacao = false; mostrandoModalConfirmacao = false;
// Aguardar um pouco para garantir que o backend processou o registro
await new Promise((resolve) => setTimeout(resolve, 800));
// Forçar atualização das queries para mostrar o novo registro // Forçar atualização das queries para mostrar o novo registro
refreshKey++; refreshKey++;
@@ -362,11 +363,13 @@
console.log('[RegistroPonto] Registro bem-sucedido, refreshKey incrementado:', refreshKey); console.log('[RegistroPonto] Registro bem-sucedido, refreshKey incrementado:', refreshKey);
} }
// Aguardar um pouco para garantir que o backend processou o registro // Aguardar mais um pouco e forçar outra atualização para garantir sincronização completa
await new Promise((resolve) => setTimeout(resolve, 500)); setTimeout(() => {
refreshKey++;
// Forçar mais uma atualização após o delay para garantir sincronização if (import.meta.env.DEV) {
refreshKey++; console.log('[RegistroPonto] Segunda atualização, refreshKey incrementado:', refreshKey);
}
}, 1500);
// Mostrar comprovante após 1 segundo // Mostrar comprovante após 1 segundo
setTimeout(() => { setTimeout(() => {
@@ -500,25 +503,27 @@
timestampBase = Date.now(); timestampBase = Date.now();
} }
// Aplicar GMT offset ao timestamp // Aplicar GMT offset ao timestamp (o horário já vem corrigido do servidor)
// Quando GMT é 0, usar timestamp UTC puro e deixar toLocaleTimeString() fazer a conversão automática // Apenas aplicar o offset configurado, sem ajustes adicionais de timezone
// Quando GMT ≠ 0, aplicar offset configurado ao timestamp
let timestamp: number; let timestamp: number;
if (gmtOffset !== 0) { if (gmtOffset !== 0) {
// Aplicar offset configurado // Aplicar offset configurado
timestamp = timestampBase + gmtOffset * 60 * 60 * 1000; timestamp = timestampBase + gmtOffset * 60 * 60 * 1000;
} else { } else {
// Quando GMT = 0, manter timestamp UTC puro // Quando GMT = 0, usar timestamp base diretamente (já vem corrigido)
// O toLocaleTimeString() converterá automaticamente para o timezone local do navegador
timestamp = timestampBase; timestamp = timestampBase;
} }
// Usar métodos UTC diretamente para evitar conversão automática do navegador
// O timestamp já está ajustado, então formatamos como UTC para manter o valor correto
const dataObj = new Date(timestamp); const dataObj = new Date(timestamp);
const data = dataObj.toLocaleDateString('pt-BR'); const dia = String(dataObj.getUTCDate()).padStart(2, '0');
const hora = dataObj.toLocaleTimeString('pt-BR', { const mes = String(dataObj.getUTCMonth() + 1).padStart(2, '0');
hour: '2-digit', const ano = dataObj.getUTCFullYear();
minute: '2-digit', const data = `${dia}/${mes}/${ano}`;
second: '2-digit' const horaStr = String(dataObj.getUTCHours()).padStart(2, '0');
}); const minutoStr = String(dataObj.getUTCMinutes()).padStart(2, '0');
const segundoStr = String(dataObj.getUTCSeconds()).padStart(2, '0');
const hora = `${horaStr}:${minutoStr}:${segundoStr}`;
dataHoraAtual = { data, hora }; dataHoraAtual = { data, hora };
} catch (error) { } catch (error) {
console.warn('Erro ao obter tempo do servidor, usando tempo local:', error); console.warn('Erro ao obter tempo do servidor, usando tempo local:', error);
@@ -866,7 +871,8 @@
!estaDispensado && !estaDispensado &&
!emFerias && !emFerias &&
!emLicenca && !emLicenca &&
temFuncionarioAssociado temFuncionarioAssociado &&
sincronizacaoConcluida // Só permitir registro após sincronização concluída
); );
}); });
@@ -1131,24 +1137,40 @@
id="relogio-sincronizado-ref" id="relogio-sincronizado-ref"
class="card from-primary/10 to-primary/5 border-primary/20 w-full max-w-sm rounded-2xl border-2 bg-linear-to-br p-5 shadow-lg" class="card from-primary/10 to-primary/5 border-primary/20 w-full max-w-sm rounded-2xl border-2 bg-linear-to-br p-5 shadow-lg"
> >
<RelogioSincronizado /> <RelogioSincronizado bind:sincronizacaoConcluida />
</div> </div>
</div> </div>
<!-- Mensagem de Aguarde Sincronização -->
{#if !sincronizacaoConcluida}
<div class="alert alert-info mb-5 rounded-xl shadow-lg">
<span class="loading loading-spinner loading-sm text-info"></span>
<div>
<h3 class="font-bold">Aguarde a sincronização</h3>
<div class="text-sm">
O sistema está sincronizando o horário com o servidor. O botão de registro será habilitado
após a conclusão da sincronização.
</div>
</div>
</div>
{/if}
<!-- Botão de Registro --> <!-- Botão de Registro -->
<button <button
class="btn btn-primary mb-5 w-full gap-2 rounded-xl font-semibold shadow-lg transition-all duration-300 hover:shadow-xl" class="btn btn-primary mb-5 w-full gap-2 rounded-xl font-semibold shadow-lg transition-all duration-300 hover:shadow-xl"
onclick={iniciarRegistroComFoto} onclick={iniciarRegistroComFoto}
disabled={!podeRegistrar} disabled={!podeRegistrar}
title={!temFuncionarioAssociado title={!sincronizacaoConcluida
? 'Você não possui funcionário associado à sua conta' ? 'Aguarde a sincronização do horário com o servidor'
: estaDispensado : !temFuncionarioAssociado
? 'Você está dispensado de registrar ponto no momento' ? 'Você não possui funcionário associado à sua conta'
: emFerias : estaDispensado
? 'Você está em férias. Durante o período de férias não é permitido registrar ponto.' ? 'Você está dispensado de registrar ponto no momento'
: emLicenca : emFerias
? 'Você está em licença. Durante o período de licença não é permitido registrar ponto.' ? 'Você está em férias. Durante o período de férias não é permitido registrar ponto.'
: ''} : emLicenca
? 'Você está em licença. Durante o período de licença não é permitido registrar ponto.'
: ''}
> >
{#if registrando} {#if registrando}
<span class="loading loading-spinner loading-sm"></span> <span class="loading loading-spinner loading-sm"></span>
@@ -1157,6 +1179,9 @@
{:else} {:else}
Registrando... Registrando...
{/if} {/if}
{:else if !sincronizacaoConcluida}
<span class="loading loading-spinner loading-sm"></span>
Aguardando Sincronização
{:else if !temFuncionarioAssociado} {:else if !temFuncionarioAssociado}
<XCircle class="h-5 w-5" /> <XCircle class="h-5 w-5" />
Funcionário Não Associado Funcionário Não Associado

View File

@@ -7,6 +7,9 @@
const client = useConvexClient(); const client = useConvexClient();
// Expor estados para o componente pai usando $props() do Svelte 5
let { sincronizacaoConcluida = $bindable(false) }: { sincronizacaoConcluida: boolean } = $props();
let tempoAtual = $state<Date>(new Date()); let tempoAtual = $state<Date>(new Date());
let sincronizado = $state(false); let sincronizado = $state(false);
let sincronizando = $state(false); let sincronizando = $state(false);
@@ -16,6 +19,7 @@
let intervalId: ReturnType<typeof setInterval> | null = null; let intervalId: ReturnType<typeof setInterval> | null = null;
let intervaloSincronizacao: 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 let sincronizacaoEmAndamento = $state(false); // Flag para evitar múltiplas sincronizações simultâneas
let sincronizacaoInicialConcluida = $state(false); // Flag para indicar que a primeira sincronização foi concluída
async function atualizarTempo() { async function atualizarTempo() {
// Evitar múltiplas sincronizações simultâneas // Evitar múltiplas sincronizações simultâneas
@@ -92,6 +96,11 @@
} finally { } finally {
sincronizando = false; sincronizando = false;
sincronizacaoEmAndamento = false; sincronizacaoEmAndamento = false;
// Marcar sincronização inicial como concluída após a primeira tentativa
if (!sincronizacaoInicialConcluida) {
sincronizacaoInicialConcluida = true;
sincronizacaoConcluida = true;
}
} }
} }
@@ -106,6 +115,7 @@
tempoAtual = new Date(obterTempoPC()); tempoAtual = new Date(obterTempoPC());
sincronizado = false; sincronizado = false;
erro = 'Usando servidor interno'; erro = 'Usando servidor interno';
sincronizacaoConcluida = false; // Garantir que começa como false
// Atualizar display a cada segundo // Atualizar display a cada segundo
intervalId = setInterval(atualizarRelogio, 1000); intervalId = setInterval(atualizarRelogio, 1000);
// Sincronizar em background (não bloquear) após um pequeno delay para garantir que a UI está renderizada // Sincronizar em background (não bloquear) após um pequeno delay para garantir que a UI está renderizada