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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
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,9 +234,12 @@
|
||||
videoElement.addEventListener('playing', onPlaying);
|
||||
videoElement.addEventListener('error', onError);
|
||||
|
||||
// Tentar reproduzir
|
||||
// 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) {
|
||||
@@ -231,7 +249,11 @@
|
||||
}
|
||||
})
|
||||
.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(() => {
|
||||
@@ -248,6 +270,14 @@
|
||||
}, 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);
|
||||
|
||||
@@ -20,8 +20,13 @@
|
||||
const client = useConvexClient();
|
||||
|
||||
// Estados
|
||||
let dataInicio = $state(new Date().toISOString().split('T')[0]!);
|
||||
let dataFim = $state(new Date().toISOString().split('T')[0]!);
|
||||
// Expandir período padrão para últimos 30 dias para facilitar visualização
|
||||
const hoje = new Date();
|
||||
const trintaDiasAtras = new Date(hoje);
|
||||
trintaDiasAtras.setDate(hoje.getDate() - 30);
|
||||
|
||||
let dataInicio = $state(trintaDiasAtras.toISOString().split('T')[0]!);
|
||||
let dataFim = $state(hoje.toISOString().split('T')[0]!);
|
||||
let funcionarioIdFiltro = $state<Id<'funcionarios'> | ''>('');
|
||||
let statusFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
|
||||
let localizacaoFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
|
||||
@@ -56,6 +61,21 @@
|
||||
const estatisticas = $derived(estatisticasQuery?.data);
|
||||
const config = $derived(configQuery?.data);
|
||||
|
||||
// Debug: Log dos dados recebidos
|
||||
$effect(() => {
|
||||
if (registrosQuery !== undefined) {
|
||||
console.log('[Frontend] registrosQuery:', {
|
||||
isLoading: registrosQuery?.isLoading,
|
||||
error: registrosQuery?.error,
|
||||
dataLength: registrosQuery?.data?.length ?? 0,
|
||||
params: registrosParams
|
||||
});
|
||||
}
|
||||
if (registros && registros.length > 0) {
|
||||
console.log('[Frontend] Primeiros registros:', registros.slice(0, 3));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Dados do gráfico baseados nas estatísticas
|
||||
const chartData = $derived.by(() => {
|
||||
@@ -514,8 +534,12 @@
|
||||
|
||||
// Função para limpar todos os filtros
|
||||
function limparFiltros() {
|
||||
dataInicio = new Date().toISOString().split('T')[0]!;
|
||||
dataFim = new Date().toISOString().split('T')[0]!;
|
||||
const hoje = new Date();
|
||||
const trintaDiasAtras = new Date(hoje);
|
||||
trintaDiasAtras.setDate(hoje.getDate() - 30);
|
||||
|
||||
dataInicio = trintaDiasAtras.toISOString().split('T')[0]!;
|
||||
dataFim = hoje.toISOString().split('T')[0]!;
|
||||
funcionarioIdFiltro = '';
|
||||
statusFiltro = 'todos';
|
||||
localizacaoFiltro = 'todos';
|
||||
|
||||
@@ -791,6 +791,7 @@ export const registrarPonto = mutation({
|
||||
export const listarRegistrosDia = query({
|
||||
args: {
|
||||
data: v.optional(v.string()), // YYYY-MM-DD, se não fornecido usa hoje
|
||||
_refresh: v.optional(v.number()), // Parâmetro usado pelo frontend para forçar refresh
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -801,12 +802,22 @@ export const listarRegistrosDia = query({
|
||||
const funcionarioId = usuario.funcionarioId; // Garantir que não é undefined
|
||||
const data = args.data || new Date().toISOString().split('T')[0]!;
|
||||
|
||||
console.log('[listarRegistrosDia] Buscando registros:', { funcionarioId, data });
|
||||
|
||||
const registros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
||||
.order('asc')
|
||||
.collect();
|
||||
|
||||
console.log('[listarRegistrosDia] Registros encontrados:', registros.length, registros.map(r => ({
|
||||
_id: r._id,
|
||||
tipo: r.tipo,
|
||||
data: r.data,
|
||||
hora: r.hora,
|
||||
minuto: r.minuto
|
||||
})));
|
||||
|
||||
return registros;
|
||||
},
|
||||
});
|
||||
@@ -862,7 +873,7 @@ export const listarRegistrosPeriodo = query({
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
// Retornar array vazio quando não autenticado
|
||||
console.warn('[listarRegistrosPeriodo] Usuário não autenticado');
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -872,15 +883,27 @@ export const listarRegistrosPeriodo = query({
|
||||
|
||||
// Validar formato das datas
|
||||
if (!args.dataInicio || !args.dataFim) {
|
||||
console.warn('[listarRegistrosPeriodo] Datas não fornecidas');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Validar formato YYYY-MM-DD
|
||||
const dataInicioRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dataInicioRegex.test(args.dataInicio) || !dataInicioRegex.test(args.dataFim)) {
|
||||
console.warn('[listarRegistrosPeriodo] Formato de data inválido', {
|
||||
dataInicio: args.dataInicio,
|
||||
dataFim: args.dataFim
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Buscando registros', {
|
||||
dataInicio: args.dataInicio,
|
||||
dataFim: args.dataFim,
|
||||
funcionarioId: args.funcionarioId,
|
||||
usuarioId: usuario._id
|
||||
});
|
||||
|
||||
let registrosFiltrados;
|
||||
|
||||
// Se funcionário foi especificado, usar índice por funcionário e data (mais eficiente)
|
||||
@@ -901,7 +924,8 @@ export const listarRegistrosPeriodo = query({
|
||||
});
|
||||
} else {
|
||||
// Se não há funcionário especificado, buscar todos e filtrar (menos eficiente, mas necessário)
|
||||
// Usar comparação de strings diretamente para datas no formato YYYY-MM-DD
|
||||
try {
|
||||
// Tentar usar índice por data primeiro
|
||||
const registros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_data', (q) =>
|
||||
@@ -909,12 +933,31 @@ export const listarRegistrosPeriodo = query({
|
||||
)
|
||||
.collect();
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Registros do índice by_data:', registros.length);
|
||||
|
||||
// Garantir que as datas estão no formato correto e filtrar novamente para garantir
|
||||
registrosFiltrados = registros.filter((r) => {
|
||||
// Comparação de strings funciona para formato YYYY-MM-DD
|
||||
return r.data >= args.dataInicio && r.data <= args.dataFim;
|
||||
});
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Registros após filtro:', registrosFiltrados.length);
|
||||
} catch (error) {
|
||||
console.error('[listarRegistrosPeriodo] Erro ao buscar registros:', error);
|
||||
// Fallback: buscar todos e filtrar manualmente
|
||||
const todosRegistros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.collect();
|
||||
|
||||
registrosFiltrados = todosRegistros.filter((r) => {
|
||||
return r.data >= args.dataInicio && r.data <= args.dataFim;
|
||||
});
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Fallback - registros encontrados:', registrosFiltrados.length);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Registros encontrados antes de buscar funcionários:', registrosFiltrados.length);
|
||||
|
||||
// Buscar informações dos funcionários
|
||||
const funcionariosIds = new Set(registrosFiltrados.map((r) => r.funcionarioId));
|
||||
@@ -940,6 +983,8 @@ export const listarRegistrosPeriodo = query({
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Total de registros a retornar:', registrosFiltrados.length);
|
||||
|
||||
return registrosFiltrados.map((registro) => {
|
||||
const funcionario = funcionarios.find((f) => f?._id === registro.funcionarioId);
|
||||
const chave = `${registro.funcionarioId}-${registro.data}`;
|
||||
@@ -1225,11 +1270,19 @@ export const obterHistoricoESaldoDia = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
data: v.string(), // YYYY-MM-DD
|
||||
_refresh: v.optional(v.number()), // Parâmetro usado pelo frontend para forçar refresh
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario || !usuario.funcionarioId) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
console.warn('[obterHistoricoESaldoDia] Usuário não autenticado ou sem funcionarioId');
|
||||
// Retornar dados vazios em vez de lançar erro
|
||||
return {
|
||||
registros: [],
|
||||
cargaHorariaDiaria: 0,
|
||||
horasTrabalhadas: 0,
|
||||
saldoMinutos: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se é o próprio funcionário ou tem permissão
|
||||
@@ -1246,6 +1299,11 @@ export const obterHistoricoESaldoDia = query({
|
||||
.order('asc')
|
||||
.collect();
|
||||
|
||||
console.log('[obterHistoricoESaldoDia] Registros encontrados:', registros.length, {
|
||||
funcionarioId: args.funcionarioId,
|
||||
data: args.data
|
||||
});
|
||||
|
||||
// Buscar configuração de ponto
|
||||
const config = await ctx.db
|
||||
.query('configuracaoPonto')
|
||||
|
||||
Reference in New Issue
Block a user