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>
|
</script>
|
||||||
|
|
||||||
<div class="modal modal-open" style="display: flex; align-items: center; justify-content: center;">
|
<div
|
||||||
<div class="modal-box max-w-2xl w-[95%] max-h-[85vh] overflow-hidden flex flex-col" style="margin: auto; max-height: 85vh;">
|
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 -->
|
<!-- Header fixo -->
|
||||||
<div class="flex items-center justify-between mb-4 pb-4 border-b border-base-300 flex-shrink-0">
|
<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}>
|
<button class="btn btn-sm btn-circle btn-ghost hover:bg-base-300" onclick={onClose}>
|
||||||
<X class="h-5 w-5" />
|
<X class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Conteúdo com rolagem -->
|
<!-- 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}
|
{#if registroQuery === undefined}
|
||||||
<div class="flex justify-center items-center py-8">
|
<div class="flex justify-center items-center py-8">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
@@ -339,6 +357,54 @@
|
|||||||
<button class="btn btn-outline" onclick={onClose}>Fechar</button>
|
<button class="btn btn-outline" onclick={onClose}>Fechar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-backdrop" onclick={onClose}></div>
|
|
||||||
</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();
|
const client = useConvexClient();
|
||||||
|
|
||||||
|
// Chave de refresh para forçar atualização das queries após registro
|
||||||
|
let refreshKey = $state(0);
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
||||||
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
|
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
|
||||||
const registrosHojeQuery = useQuery(api.pontos.listarRegistrosDia, {});
|
|
||||||
|
|
||||||
// Query para histórico e saldo do dia
|
// Query para histórico e saldo do dia
|
||||||
const funcionarioId = $derived(currentUser?.data?.funcionarioId ?? null);
|
const funcionarioId = $derived(currentUser?.data?.funcionarioId ?? null);
|
||||||
const dataHoje = $derived(new Date().toISOString().split('T')[0]!);
|
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(
|
const historicoSaldoQuery = useQuery(
|
||||||
api.pontos.obterHistoricoESaldoDia,
|
api.pontos.obterHistoricoESaldoDia,
|
||||||
funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip'
|
funcionarioId && dataHoje ? { funcionarioId, data: dataHoje, _refresh: refreshKey } : 'skip'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Query para verificar dispensa ativa
|
// Query para verificar dispensa ativa
|
||||||
@@ -284,6 +293,19 @@
|
|||||||
justificativa = ''; // Limpar justificativa após registro
|
justificativa = ''; // Limpar justificativa após registro
|
||||||
mostrandoModalConfirmacao = false;
|
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
|
// Mostrar comprovante após 1 segundo
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mostrandoComprovante = true;
|
mostrandoComprovante = true;
|
||||||
@@ -799,7 +821,7 @@
|
|||||||
{ tipo: 'saida', horario: config.horarioSaida, label: config.nomeSaida || 'Saída 2' }
|
{ 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);
|
const registro = registrosHoje.find((r) => r.tipo === h.tipo);
|
||||||
return {
|
return {
|
||||||
...h,
|
...h,
|
||||||
@@ -808,6 +830,17 @@
|
|||||||
dentroDoPrazo: registro?.dentroDoPrazo ?? null
|
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
|
// Dados do histórico e saldo
|
||||||
|
|||||||
@@ -22,18 +22,29 @@
|
|||||||
let previewUrl = $state<string | null>(null);
|
let previewUrl = $state<string | null>(null);
|
||||||
let videoReady = $state(false);
|
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
|
// Efeito para garantir que o vídeo seja exibido quando o stream for atribuído
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (stream && videoElement) {
|
if (stream && videoElement && !playEmAndamento) {
|
||||||
// Sempre atualizar srcObject quando o stream mudar
|
// Sempre atualizar srcObject quando o stream mudar
|
||||||
if (videoElement.srcObject !== stream) {
|
if (videoElement.srcObject !== stream) {
|
||||||
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) {
|
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()
|
videoElement.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
playEmAndamento = false;
|
||||||
// Aguardar um pouco para garantir que o vídeo esteja realmente reproduzindo
|
// Aguardar um pouco para garantir que o vídeo esteja realmente reproduzindo
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
||||||
@@ -42,7 +53,11 @@
|
|||||||
}, 300);
|
}, 300);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.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) {
|
} else if (videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
||||||
videoReady = true;
|
videoReady = true;
|
||||||
@@ -219,35 +234,50 @@
|
|||||||
videoElement.addEventListener('playing', onPlaying);
|
videoElement.addEventListener('playing', onPlaying);
|
||||||
videoElement.addEventListener('error', onError);
|
videoElement.addEventListener('error', onError);
|
||||||
|
|
||||||
// Tentar reproduzir
|
// Tentar reproduzir apenas se não estiver já reproduzindo
|
||||||
videoElement.play()
|
if (videoElement.paused) {
|
||||||
.then(() => {
|
playEmAndamento = true;
|
||||||
console.log('Vídeo iniciado, readyState:', videoElement?.readyState);
|
videoElement.play()
|
||||||
// Se já tiver metadata e dimensões, resolver imediatamente
|
.then(() => {
|
||||||
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
playEmAndamento = false;
|
||||||
setTimeout(() => {
|
console.log('Vídeo iniciado, readyState:', videoElement?.readyState);
|
||||||
onLoadedMetadata();
|
// Se já tiver metadata e dimensões, resolver imediatamente
|
||||||
}, 300);
|
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
||||||
}
|
setTimeout(() => {
|
||||||
})
|
|
||||||
.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) {
|
|
||||||
onLoadedMetadata();
|
onLoadedMetadata();
|
||||||
} else {
|
}, 300);
|
||||||
onError();
|
}
|
||||||
}
|
})
|
||||||
}, 1000);
|
.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);
|
console.log('Vídeo pronto, dimensões:', videoElement.videoWidth, 'x', videoElement.videoHeight);
|
||||||
|
|||||||
@@ -20,8 +20,13 @@
|
|||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
// Estados
|
// Estados
|
||||||
let dataInicio = $state(new Date().toISOString().split('T')[0]!);
|
// Expandir período padrão para últimos 30 dias para facilitar visualização
|
||||||
let dataFim = $state(new Date().toISOString().split('T')[0]!);
|
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 funcionarioIdFiltro = $state<Id<'funcionarios'> | ''>('');
|
||||||
let statusFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
|
let statusFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
|
||||||
let localizacaoFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
|
let localizacaoFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
|
||||||
@@ -56,6 +61,21 @@
|
|||||||
const estatisticas = $derived(estatisticasQuery?.data);
|
const estatisticas = $derived(estatisticasQuery?.data);
|
||||||
const config = $derived(configQuery?.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
|
// Dados do gráfico baseados nas estatísticas
|
||||||
const chartData = $derived.by(() => {
|
const chartData = $derived.by(() => {
|
||||||
@@ -514,8 +534,12 @@
|
|||||||
|
|
||||||
// Função para limpar todos os filtros
|
// Função para limpar todos os filtros
|
||||||
function limparFiltros() {
|
function limparFiltros() {
|
||||||
dataInicio = new Date().toISOString().split('T')[0]!;
|
const hoje = new Date();
|
||||||
dataFim = new Date().toISOString().split('T')[0]!;
|
const trintaDiasAtras = new Date(hoje);
|
||||||
|
trintaDiasAtras.setDate(hoje.getDate() - 30);
|
||||||
|
|
||||||
|
dataInicio = trintaDiasAtras.toISOString().split('T')[0]!;
|
||||||
|
dataFim = hoje.toISOString().split('T')[0]!;
|
||||||
funcionarioIdFiltro = '';
|
funcionarioIdFiltro = '';
|
||||||
statusFiltro = 'todos';
|
statusFiltro = 'todos';
|
||||||
localizacaoFiltro = 'todos';
|
localizacaoFiltro = 'todos';
|
||||||
|
|||||||
@@ -791,6 +791,7 @@ export const registrarPonto = mutation({
|
|||||||
export const listarRegistrosDia = query({
|
export const listarRegistrosDia = query({
|
||||||
args: {
|
args: {
|
||||||
data: v.optional(v.string()), // YYYY-MM-DD, se não fornecido usa hoje
|
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) => {
|
handler: async (ctx, args) => {
|
||||||
const usuario = await getCurrentUserFunction(ctx);
|
const usuario = await getCurrentUserFunction(ctx);
|
||||||
@@ -801,12 +802,22 @@ export const listarRegistrosDia = query({
|
|||||||
const funcionarioId = usuario.funcionarioId; // Garantir que não é undefined
|
const funcionarioId = usuario.funcionarioId; // Garantir que não é undefined
|
||||||
const data = args.data || new Date().toISOString().split('T')[0]!;
|
const data = args.data || new Date().toISOString().split('T')[0]!;
|
||||||
|
|
||||||
|
console.log('[listarRegistrosDia] Buscando registros:', { funcionarioId, data });
|
||||||
|
|
||||||
const registros = await ctx.db
|
const registros = await ctx.db
|
||||||
.query('registrosPonto')
|
.query('registrosPonto')
|
||||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
||||||
.order('asc')
|
.order('asc')
|
||||||
.collect();
|
.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;
|
return registros;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -862,7 +873,7 @@ export const listarRegistrosPeriodo = query({
|
|||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const usuario = await getCurrentUserFunction(ctx);
|
const usuario = await getCurrentUserFunction(ctx);
|
||||||
if (!usuario) {
|
if (!usuario) {
|
||||||
// Retornar array vazio quando não autenticado
|
console.warn('[listarRegistrosPeriodo] Usuário não autenticado');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -872,15 +883,27 @@ export const listarRegistrosPeriodo = query({
|
|||||||
|
|
||||||
// Validar formato das datas
|
// Validar formato das datas
|
||||||
if (!args.dataInicio || !args.dataFim) {
|
if (!args.dataInicio || !args.dataFim) {
|
||||||
|
console.warn('[listarRegistrosPeriodo] Datas não fornecidas');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validar formato YYYY-MM-DD
|
// Validar formato YYYY-MM-DD
|
||||||
const dataInicioRegex = /^\d{4}-\d{2}-\d{2}$/;
|
const dataInicioRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
if (!dataInicioRegex.test(args.dataInicio) || !dataInicioRegex.test(args.dataFim)) {
|
if (!dataInicioRegex.test(args.dataInicio) || !dataInicioRegex.test(args.dataFim)) {
|
||||||
|
console.warn('[listarRegistrosPeriodo] Formato de data inválido', {
|
||||||
|
dataInicio: args.dataInicio,
|
||||||
|
dataFim: args.dataFim
|
||||||
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[listarRegistrosPeriodo] Buscando registros', {
|
||||||
|
dataInicio: args.dataInicio,
|
||||||
|
dataFim: args.dataFim,
|
||||||
|
funcionarioId: args.funcionarioId,
|
||||||
|
usuarioId: usuario._id
|
||||||
|
});
|
||||||
|
|
||||||
let registrosFiltrados;
|
let registrosFiltrados;
|
||||||
|
|
||||||
// Se funcionário foi especificado, usar índice por funcionário e data (mais eficiente)
|
// Se funcionário foi especificado, usar índice por funcionário e data (mais eficiente)
|
||||||
@@ -901,21 +924,41 @@ export const listarRegistrosPeriodo = query({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Se não há funcionário especificado, buscar todos e filtrar (menos eficiente, mas necessário)
|
// 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 {
|
||||||
const registros = await ctx.db
|
// Tentar usar índice por data primeiro
|
||||||
.query('registrosPonto')
|
const registros = await ctx.db
|
||||||
.withIndex('by_data', (q) =>
|
.query('registrosPonto')
|
||||||
q.gte('data', args.dataInicio).lte('data', args.dataFim)
|
.withIndex('by_data', (q) =>
|
||||||
)
|
q.gte('data', args.dataInicio).lte('data', args.dataFim)
|
||||||
.collect();
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Garantir que as datas estão no formato correto e filtrar novamente para garantir
|
console.log('[listarRegistrosPeriodo] Registros do índice by_data:', registros.length);
|
||||||
registrosFiltrados = registros.filter((r) => {
|
|
||||||
// Comparação de strings funciona para formato YYYY-MM-DD
|
// Garantir que as datas estão no formato correto e filtrar novamente para garantir
|
||||||
return r.data >= args.dataInicio && r.data <= args.dataFim;
|
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
|
// Buscar informações dos funcionários
|
||||||
const funcionariosIds = new Set(registrosFiltrados.map((r) => r.funcionarioId));
|
const funcionariosIds = new Set(registrosFiltrados.map((r) => r.funcionarioId));
|
||||||
const funcionarios = await Promise.all(
|
const funcionarios = await Promise.all(
|
||||||
@@ -940,6 +983,8 @@ export const listarRegistrosPeriodo = query({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[listarRegistrosPeriodo] Total de registros a retornar:', registrosFiltrados.length);
|
||||||
|
|
||||||
return registrosFiltrados.map((registro) => {
|
return registrosFiltrados.map((registro) => {
|
||||||
const funcionario = funcionarios.find((f) => f?._id === registro.funcionarioId);
|
const funcionario = funcionarios.find((f) => f?._id === registro.funcionarioId);
|
||||||
const chave = `${registro.funcionarioId}-${registro.data}`;
|
const chave = `${registro.funcionarioId}-${registro.data}`;
|
||||||
@@ -1225,11 +1270,19 @@ export const obterHistoricoESaldoDia = query({
|
|||||||
args: {
|
args: {
|
||||||
funcionarioId: v.id('funcionarios'),
|
funcionarioId: v.id('funcionarios'),
|
||||||
data: v.string(), // YYYY-MM-DD
|
data: v.string(), // YYYY-MM-DD
|
||||||
|
_refresh: v.optional(v.number()), // Parâmetro usado pelo frontend para forçar refresh
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const usuario = await getCurrentUserFunction(ctx);
|
const usuario = await getCurrentUserFunction(ctx);
|
||||||
if (!usuario || !usuario.funcionarioId) {
|
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
|
// Verificar se é o próprio funcionário ou tem permissão
|
||||||
@@ -1246,6 +1299,11 @@ export const obterHistoricoESaldoDia = query({
|
|||||||
.order('asc')
|
.order('asc')
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
console.log('[obterHistoricoESaldoDia] Registros encontrados:', registros.length, {
|
||||||
|
funcionarioId: args.funcionarioId,
|
||||||
|
data: args.data
|
||||||
|
});
|
||||||
|
|
||||||
// Buscar configuração de ponto
|
// Buscar configuração de ponto
|
||||||
const config = await ctx.db
|
const config = await ctx.db
|
||||||
.query('configuracaoPonto')
|
.query('configuracaoPonto')
|
||||||
|
|||||||
Reference in New Issue
Block a user