From 5944b4ddfa6c940fc2eb481e1cd157400bf7d13b Mon Sep 17 00:00:00 2001 From: Santserrano Date: Mon, 17 Nov 2025 12:59:06 -0300 Subject: [PATCH] updates --- .../app/colaborador/scanner/page.tsx | 76 +++------- .../components/qr-scanner-free-event.tsx | 131 +++++++----------- .../hooks/use-colaborador-eventos.ts | 36 ++++- .../hooks/use-free-event-scanner.ts | 85 ++++++++++++ 4 files changed, 194 insertions(+), 134 deletions(-) create mode 100644 apps/next-frontend/hooks/use-free-event-scanner.ts diff --git a/apps/next-frontend/app/colaborador/scanner/page.tsx b/apps/next-frontend/app/colaborador/scanner/page.tsx index 3a9f616..82c257f 100644 --- a/apps/next-frontend/app/colaborador/scanner/page.tsx +++ b/apps/next-frontend/app/colaborador/scanner/page.tsx @@ -4,6 +4,10 @@ import { QRScanner } from '@/components/qr-scanner'; import { QRScannerFreeEvent } from '@/components/qr-scanner-free-event'; import { InviteCodeModal } from '@/components/invite-code-modal'; import { useRoleProtection } from '@/hooks/use-role-protection'; +import { + useGetMyColaboradorEvents, + useUseColaboradorInviteCode, +} from '@/hooks/use-colaborador-eventos'; import { useRouter } from 'next/navigation'; import { Loader2 } from 'lucide-react'; import { useEffect, useState } from 'react'; @@ -12,43 +16,26 @@ import { toast } from 'sonner'; export default function ScannerPage() { const { isProtected, isLoading: sessionLoading } = useRoleProtection(['COLABORADOR']); const router = useRouter(); - const [eventos, setEventos] = useState([]); - const [eventInfo, setEventInfo] = useState(null); - const [loading, setLoading] = useState(true); const [showCodeModal, setShowCodeModal] = useState(false); const [inviteCodeInput, setInviteCodeInput] = useState(''); - const [isUsingCode, setIsUsingCode] = useState(false); - // Cargar eventos del colaborador - useEffect(() => { - loadEventos(); - }, []); + const { + data: eventosData, + isLoading: eventosLoading, + error: eventosError, + } = useGetMyColaboradorEvents(); - const loadEventos = async () => { - try { - setLoading(true); - const response = await fetch('/api/colaborador/mis-eventos'); - - if (!response.ok) { - console.error('Error loading eventos:', response.statusText); - return; - } - - const data = await response.json(); - console.log('📍 Eventos del colaborador:', data.eventos); - setEventos(data.eventos || []); - - // Cargar info del primer evento - if (data.eventos && data.eventos.length > 0) { - setEventInfo(data.eventos[0]); - } - } catch (error) { - console.error('Error loading eventos:', error); - toast.error('Error al cargar los eventos'); - } finally { - setLoading(false); + const useInviteCodeMutation = useUseColaboradorInviteCode(); + const eventos = eventosData ?? []; + const eventInfo = eventos[0] ?? null; + const isUsingCode = useInviteCodeMutation.isPending; + const isEventosLoading = eventosLoading && !eventosData; + + useEffect(() => { + if (eventosError instanceof Error) { + toast.error(eventosError.message); } - }; + }, [eventosError]); const handleUseInviteCode = async () => { if (!inviteCodeInput.trim()) { @@ -56,35 +43,14 @@ export default function ScannerPage() { return; } - setIsUsingCode(true); try { - const response = await fetch('/api/invite-codes/use', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - codigo: inviteCodeInput.trim(), - }), - }); - - const data = await response.json(); - - if (!response.ok) { - toast.error(data.error || 'Error al validar código'); - return; - } + const data = await useInviteCodeMutation.mutateAsync(inviteCodeInput.trim()); toast.success(data.message || '¡Código validado! Uniendo al evento...'); setInviteCodeInput(''); setShowCodeModal(false); - - // Recargar eventos después de usar el código - await loadEventos(); } catch (error) { toast.error(error instanceof Error ? error.message : 'Error al validar código'); - } finally { - setIsUsingCode(false); } }; @@ -102,7 +68,7 @@ export default function ScannerPage() { } // Verificar que el colaborador tiene un evento asignado - if (loading) { + if (isEventosLoading) { return (
diff --git a/apps/next-frontend/components/qr-scanner-free-event.tsx b/apps/next-frontend/components/qr-scanner-free-event.tsx index 4ab4184..44a5265 100644 --- a/apps/next-frontend/components/qr-scanner-free-event.tsx +++ b/apps/next-frontend/components/qr-scanner-free-event.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { CheckCircle, QrCode, AlertCircle, Loader2, Search, Users, X } from 'lucide-react'; import { toast } from 'sonner'; import { Html5Qrcode } from 'html5-qrcode'; +import { useFreeEventInscripciones, useValidateFreeEventQr } from '@/hooks/use-free-event-scanner'; interface Inscripcion { id: string; @@ -164,22 +165,34 @@ function CameraScannerModal({ isOpen, onClose, onScan }: CameraScannerModalProps export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) { const [manualCode, setManualCode] = useState(''); - const [inscripciones, setInscripciones] = useState([]); - const [estadisticas, setEstadisticas] = useState({ - totalInscritos: 0, - validados: 0, - pendientes: 0, - }); - const [loading, setLoading] = useState(true); - const [scanning, setScanning] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [showCameraModal, setShowCameraModal] = useState(false); const inputRef = useRef(null); + const isProcessingScanRef = useRef(false); + const lastScanRef = useRef<{ code: string; timestamp: number } | null>(null); + + const { + data: scannerData, + isLoading: isScannerLoading, + error: scannerError, + } = useFreeEventInscripciones(eventoid); + + const validateQrMutation = useValidateFreeEventQr(eventoid); + const scanning = validateQrMutation.isPending; + + const estadisticas = scannerData?.estadisticas ?? { + totalInscritos: 0, + validados: 0, + pendientes: 0, + }; + const inscripciones = scannerData?.inscripciones ?? []; + const isInitialLoading = isScannerLoading && !scannerData; - // Cargar inscripciones al montar useEffect(() => { - loadInscripciones(); - }, [eventoid]); + if (scannerError instanceof Error) { + toast.error(scannerError.message); + } + }, [scannerError]); // Auto-focus en el input useEffect(() => { @@ -188,76 +201,43 @@ export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) { } }, []); - const loadInscripciones = async () => { - try { - setLoading(true); - const response = await fetch(`/api/validar-qr?eventId=${eventoid}`); - - if (!response.ok) { - console.error('Error al cargar inscripciones:', response.status); - return; - } - - const data = await response.json(); - console.log('🔍 Scanner: Data recibida del servidor:', data); - console.log('🔍 Scanner: Inscripciones:', data.data.inscripciones); + const handleScan = async (codigo: string) => { + const trimmedCode = codigo.trim(); + if (!trimmedCode || isProcessingScanRef.current) { + return; + } - setInscripciones(data.data.inscripciones); - setEstadisticas(data.data.estadisticas); - } catch (error) { - console.error('Error al cargar inscripciones:', error); - } finally { - setLoading(false); + const now = Date.now(); + const lastScan = lastScanRef.current; + if (lastScan && lastScan.code === trimmedCode && now - lastScan.timestamp < 2000) { + return; } - }; + lastScanRef.current = { code: trimmedCode, timestamp: now }; - const handleScan = async (codigo: string) => { - const normalizedCode = codigo.trim().toUpperCase(); - if (!normalizedCode) return; + isProcessingScanRef.current = true; setManualCode(''); - setScanning(true); - setShowCameraModal(false); try { - const response = await fetch('/api/validar-qr', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - eventId: eventoid, - codigo: normalizedCode, - }), - }); - - const data = await response.json(); - - if (!response.ok) { - toast.error(data.message || 'Error al validar código'); - } else { - if (data.data.validado) { - // Verificar si ya estaba validado o es la primera vez - const esYaValidado = data.message && data.message.includes('ya fue validado'); - - if (esYaValidado) { - toast.info( - `⚠️ ${data.data.inscripcion.nombre} - Ya validado el ${new Date(data.data.fecha_validacion).toLocaleString('es-AR')}`, - ); - } else { - toast.success(`✓ ${data.data.inscripcion.nombre} - Entrada validada`); - } - } - // Recargar la lista (sin mostrar errores si falla) - try { - await loadInscripciones(); - } catch (error) { - console.error('Error reloading inscripciones:', error); + const data = await validateQrMutation.mutateAsync(trimmedCode); + + if (data?.data?.validado) { + const esYaValidado = data.message?.includes('ya fue validado'); + + if (esYaValidado) { + toast.info( + `⚠️ ${data.data.inscripcion.nombre} - Ya validado el ${new Date(data.data.fecha_validacion || '').toLocaleString('es-AR')}`, + ); + } else { + toast.success(`✓ ${data.data.inscripcion.nombre} - Entrada validada`); } + setShowCameraModal(false); } } catch (error) { - toast.error('Error al validar código QR'); - console.error(error); + const message = error instanceof Error ? error.message : 'Error al validar código QR'; + toast.error(message); } finally { - setScanning(false); + isProcessingScanRef.current = false; if (inputRef.current) { inputRef.current.focus(); } @@ -267,6 +247,7 @@ export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) { const handleManualSubmit = (e: React.FormEvent) => { e.preventDefault(); if (manualCode.trim()) { + lastScanRef.current = null; handleScan(manualCode); } }; @@ -280,12 +261,6 @@ export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) { const inscripcionesValidadas = filteredInscripciones.filter((i) => i.validado); const inscripcionesPendientes = filteredInscripciones.filter((i) => !i.validado); - console.log('🔍 Scanner: Total inscripciones:', inscripciones.length); - console.log('🔍 Scanner: Filtradas:', filteredInscripciones.length); - console.log('🔍 Scanner: Validadas:', inscripcionesValidadas.length); - console.log('🔍 Scanner: Pendientes:', inscripcionesPendientes.length); - console.log('🔍 Scanner: searchTerm:', searchTerm); - return (
@@ -406,7 +381,7 @@ export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) {
{/* Inscripciones List */} - {loading ? ( + {isInitialLoading ? (
@@ -481,7 +456,7 @@ export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) {
)} - {filteredInscripciones.length === 0 && !loading && ( + {filteredInscripciones.length === 0 && !isInitialLoading && (

diff --git a/apps/next-frontend/hooks/use-colaborador-eventos.ts b/apps/next-frontend/hooks/use-colaborador-eventos.ts index 8216e4a..9bd2ed3 100644 --- a/apps/next-frontend/hooks/use-colaborador-eventos.ts +++ b/apps/next-frontend/hooks/use-colaborador-eventos.ts @@ -1,4 +1,11 @@ -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +interface UseInviteCodeResponse { + success: boolean; + message?: string; + eventoid?: string; + colaborador_evento_id?: string; +} export function useGetMyColaboradorEvents() { return useQuery({ @@ -18,3 +25,30 @@ export function useGetMyColaboradorEvents() { }, }); } + +export function useUseColaboradorInviteCode() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (codigo: string) => { + const response = await fetch('/api/invite-codes/use', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ codigo }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Error al validar código'); + } + + return data as UseInviteCodeResponse; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['myColaboradorEvents'] }); + }, + }); +} diff --git a/apps/next-frontend/hooks/use-free-event-scanner.ts b/apps/next-frontend/hooks/use-free-event-scanner.ts new file mode 100644 index 0000000..8f39c66 --- /dev/null +++ b/apps/next-frontend/hooks/use-free-event-scanner.ts @@ -0,0 +1,85 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +interface FreeEventInscripcion { + id: string; + nombre: string; + correo: string; + fecha_inscripcion: string; + codigoQR: string | null; + validado: boolean; + fecha_validacion: string | null; +} + +interface FreeEventStats { + totalInscritos: number; + validados: number; + pendientes: number; +} + +interface FreeEventScannerQuery { + evento?: string; + estadisticas: FreeEventStats; + inscripciones: FreeEventInscripcion[]; +} + +interface ValidateQrResponse { + message: string; + data: { + validado: boolean; + fecha_validacion: string | null; + inscripcion: { + nombre: string; + }; + }; +} + +export function useFreeEventInscripciones(eventoid: string) { + return useQuery({ + queryKey: ['free-event-inscripciones', eventoid], + enabled: !!eventoid, + queryFn: async () => { + const response = await fetch(`/api/validar-qr?eventId=${eventoid}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Error al cargar inscripciones'); + } + + return { + evento: data.data.evento, + estadisticas: data.data.estadisticas as FreeEventStats, + inscripciones: data.data.inscripciones as FreeEventInscripcion[], + }; + }, + staleTime: 30_000, + refetchOnWindowFocus: false, + }); +} + +export function useValidateFreeEventQr(eventoid: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (codigo: string) => { + const response = await fetch('/api/validar-qr', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + eventId: eventoid, + codigo, + }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Error al validar código'); + } + + return data as ValidateQrResponse; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['free-event-inscripciones', eventoid] }); + }, + }); +}