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:
@@ -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>
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|
||||||
// Forçar mais uma atualização após o delay para garantir sincronização
|
|
||||||
refreshKey++;
|
refreshKey++;
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
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,16 +1137,32 @@
|
|||||||
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
|
||||||
|
? 'Aguarde a sincronização do horário com o servidor'
|
||||||
|
: !temFuncionarioAssociado
|
||||||
? 'Você não possui funcionário associado à sua conta'
|
? 'Você não possui funcionário associado à sua conta'
|
||||||
: estaDispensado
|
: estaDispensado
|
||||||
? 'Você está dispensado de registrar ponto no momento'
|
? 'Você está dispensado de registrar ponto no momento'
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user