feat: enhance RegistroPonto and WebcamCapture components for improved data handling and user experience

- Added a refresh mechanism in the RegistroPonto component to ensure queries are updated after point registration, improving data accuracy.
- Expanded the WebcamCapture component to prevent multiple simultaneous play calls, enhancing video playback reliability.
- Updated the registro-pontos page to default the date range to the last 30 days for better visibility and user convenience.
- Introduced debug logging for queries and data handling to assist in development and troubleshooting.
This commit is contained in:
2025-11-22 23:57:05 -03:00
parent 90e81e4667
commit 467e04b605
5 changed files with 269 additions and 58 deletions

View File

@@ -237,18 +237,36 @@
}
</script>
<div class="modal modal-open" style="display: flex; align-items: center; justify-content: center;">
<div class="modal-box max-w-2xl w-[95%] max-h-[85vh] overflow-hidden flex flex-col" style="margin: auto; max-height: 85vh;">
<div
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="animation: fadeIn 0.2s ease-out;"
onkeydown={(e) => e.key === 'Escape' && onClose()}
role="dialog"
aria-modal="true"
aria-labelledby="modal-comprovante-title"
>
<!-- Backdrop com blur -->
<div
class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-200"
onclick={onClose}
></div>
<!-- Modal Box -->
<div
class="relative bg-base-100 rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col z-10 transform transition-all duration-300"
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
onclick={(e) => e.stopPropagation()}
>
<!-- Header fixo -->
<div class="flex items-center justify-between mb-4 pb-4 border-b border-base-300 flex-shrink-0">
<h3 class="font-bold text-lg">Comprovante de Registro de Ponto</h3>
<h3 id="modal-comprovante-title" class="font-bold text-lg">Comprovante de Registro de Ponto</h3>
<button class="btn btn-sm btn-circle btn-ghost hover:bg-base-300" onclick={onClose}>
<X class="h-5 w-5" />
</button>
</div>
<!-- Conteúdo com rolagem -->
<div class="flex-1 overflow-y-auto pr-2">
<div class="flex-1 overflow-y-auto pr-2 modal-scroll">
{#if registroQuery === undefined}
<div class="flex justify-center items-center py-8">
<span class="loading loading-spinner loading-lg"></span>
@@ -339,6 +357,54 @@
<button class="btn btn-outline" onclick={onClose}>Fechar</button>
</div>
</div>
<div class="modal-backdrop" onclick={onClose}></div>
</div>
<style>
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Scrollbar customizada para os modais */
:global(.modal-scroll) {
scrollbar-width: thin;
scrollbar-color: hsl(var(--bc) / 0.3) transparent;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
:global(.modal-scroll::-webkit-scrollbar) {
width: 8px;
}
:global(.modal-scroll::-webkit-scrollbar-track) {
background: transparent;
border-radius: 4px;
}
:global(.modal-scroll::-webkit-scrollbar-thumb) {
background-color: hsl(var(--bc) / 0.3);
border-radius: 4px;
transition: background-color 0.2s ease;
}
:global(.modal-scroll::-webkit-scrollbar-thumb:hover) {
background-color: hsl(var(--bc) / 0.5);
}
</style>

View File

@@ -21,17 +21,26 @@
const client = useConvexClient();
// Chave de refresh para forçar atualização das queries após registro
let refreshKey = $state(0);
// Queries
const currentUser = useQuery(api.auth.getCurrentUser, {});
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
const registrosHojeQuery = useQuery(api.pontos.listarRegistrosDia, {});
// Query para histórico e saldo do dia
const funcionarioId = $derived(currentUser?.data?.funcionarioId ?? null);
const dataHoje = $derived(new Date().toISOString().split('T')[0]!);
// Usar refreshKey para forçar atualização após registro
const registrosHojeQuery = useQuery(
api.pontos.listarRegistrosDia,
{ data: dataHoje, _refresh: refreshKey }
);
const historicoSaldoQuery = useQuery(
api.pontos.obterHistoricoESaldoDia,
funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip'
funcionarioId && dataHoje ? { funcionarioId, data: dataHoje, _refresh: refreshKey } : 'skip'
);
// Query para verificar dispensa ativa
@@ -284,6 +293,19 @@
justificativa = ''; // Limpar justificativa após registro
mostrandoModalConfirmacao = false;
// Forçar atualização das queries para mostrar o novo registro
refreshKey++;
if (import.meta.env.DEV) {
console.log('[RegistroPonto] Registro bem-sucedido, refreshKey incrementado:', refreshKey);
}
// Aguardar um pouco para garantir que o backend processou o registro
await new Promise(resolve => setTimeout(resolve, 500));
// Forçar mais uma atualização após o delay para garantir sincronização
refreshKey++;
// Mostrar comprovante após 1 segundo
setTimeout(() => {
mostrandoComprovante = true;
@@ -799,7 +821,7 @@
{ tipo: 'saida', horario: config.horarioSaida, label: config.nomeSaida || 'Saída 2' }
];
return horarios.map((h) => {
const resultado = horarios.map((h) => {
const registro = registrosHoje.find((r) => r.tipo === h.tipo);
return {
...h,
@@ -808,6 +830,17 @@
dentroDoPrazo: registro?.dentroDoPrazo ?? null
};
});
// Log para debug (apenas em desenvolvimento)
if (import.meta.env.DEV) {
console.log('[RegistroPonto] mapaHorarios atualizado:', {
totalRegistrosHoje: registrosHoje.length,
horariosComRegistro: resultado.filter(h => h.registrado).length,
registrosHoje: registrosHoje.map(r => ({ tipo: r.tipo, hora: `${r.hora}:${r.minuto}` }))
});
}
return resultado;
});
// Dados do histórico e saldo

View File

@@ -22,18 +22,29 @@
let previewUrl = $state<string | null>(null);
let videoReady = $state(false);
// Flag para evitar múltiplas chamadas de play() simultâneas
let playEmAndamento = $state(false);
// Efeito para garantir que o vídeo seja exibido quando o stream for atribuído
$effect(() => {
if (stream && videoElement) {
if (stream && videoElement && !playEmAndamento) {
// Sempre atualizar srcObject quando o stream mudar
if (videoElement.srcObject !== stream) {
videoElement.srcObject = stream;
}
// Tentar reproduzir se ainda não estiver pronto
// Tentar reproduzir se ainda não estiver pronto e não houver outra chamada em andamento
if (!videoReady && videoElement.readyState < 2) {
// Verificar se já não está reproduzindo
if (!videoElement.paused && videoElement.readyState >= 2) {
videoReady = true;
return;
}
playEmAndamento = true;
videoElement.play()
.then(() => {
playEmAndamento = false;
// Aguardar um pouco para garantir que o vídeo esteja realmente reproduzindo
setTimeout(() => {
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
@@ -42,7 +53,11 @@
}, 300);
})
.catch((err) => {
console.warn('Erro ao reproduzir vídeo no effect:', err);
playEmAndamento = false;
// Ignorar AbortError - é esperado quando há uma nova requisição de load
if (err.name !== 'AbortError') {
console.warn('Erro ao reproduzir vídeo no effect:', err);
}
});
} else if (videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
videoReady = true;
@@ -219,35 +234,50 @@
videoElement.addEventListener('playing', onPlaying);
videoElement.addEventListener('error', onError);
// Tentar reproduzir
videoElement.play()
.then(() => {
console.log('Vídeo iniciado, readyState:', videoElement?.readyState);
// Se já tiver metadata e dimensões, resolver imediatamente
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
setTimeout(() => {
onLoadedMetadata();
}, 300);
}
})
.catch((err) => {
console.warn('Erro ao reproduzir vídeo:', err);
// Continuar mesmo assim se já tiver metadata e dimensões
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
setTimeout(() => {
onLoadedMetadata();
}, 300);
} else {
// Aguardar um pouco mais antes de dar erro
setTimeout(() => {
if (videoElement && videoElement.videoWidth > 0) {
// Tentar reproduzir apenas se não estiver já reproduzindo
if (videoElement.paused) {
playEmAndamento = true;
videoElement.play()
.then(() => {
playEmAndamento = false;
console.log('Vídeo iniciado, readyState:', videoElement?.readyState);
// Se já tiver metadata e dimensões, resolver imediatamente
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
setTimeout(() => {
onLoadedMetadata();
} else {
onError();
}
}, 1000);
}
});
}, 300);
}
})
.catch((err) => {
playEmAndamento = false;
// Ignorar AbortError - é esperado quando há uma nova requisição de load
if (err.name !== 'AbortError') {
console.warn('Erro ao reproduzir vídeo:', err);
}
// Continuar mesmo assim se já tiver metadata e dimensões
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
setTimeout(() => {
onLoadedMetadata();
}, 300);
} else {
// Aguardar um pouco mais antes de dar erro
setTimeout(() => {
if (videoElement && videoElement.videoWidth > 0) {
onLoadedMetadata();
} else {
onError();
}
}, 1000);
}
});
} else {
// Já está reproduzindo, apenas verificar se está pronto
if (videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
setTimeout(() => {
onLoadedMetadata();
}, 300);
}
}
});
console.log('Vídeo pronto, dimensões:', videoElement.videoWidth, 'x', videoElement.videoHeight);