Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 21 additions & 55 deletions apps/next-frontend/app/colaborador/scanner/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -12,79 +16,41 @@ import { toast } from 'sonner';
export default function ScannerPage() {
const { isProtected, isLoading: sessionLoading } = useRoleProtection(['COLABORADOR']);
const router = useRouter();
const [eventos, setEventos] = useState<any[]>([]);
const [eventInfo, setEventInfo] = useState<any>(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()) {
toast.error('Por favor ingresa un código de invitación');
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);
}
};

Expand All @@ -102,7 +68,7 @@ export default function ScannerPage() {
}

// Verificar que el colaborador tiene un evento asignado
if (loading) {
if (isEventosLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-stone-900 to-stone-800 flex items-center justify-center">
<Loader2 size={40} className="animate-spin text-orange-500" />
Expand Down
131 changes: 53 additions & 78 deletions apps/next-frontend/components/qr-scanner-free-event.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -164,22 +165,34 @@ function CameraScannerModal({ isOpen, onClose, onScan }: CameraScannerModalProps

export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) {
const [manualCode, setManualCode] = useState('');
const [inscripciones, setInscripciones] = useState<Inscripcion[]>([]);
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<HTMLInputElement>(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(() => {
Expand All @@ -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();
}
Expand All @@ -267,6 +247,7 @@ export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) {
const handleManualSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (manualCode.trim()) {
lastScanRef.current = null;
handleScan(manualCode);
}
};
Expand All @@ -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 (
<div className="h-screen bg-gradient-to-br from-stone-900 to-stone-800 text-white p-3 md:p-6 lg:md:p-12 overflow-y-auto">
<div className="max-w-full lg:max-w-6xl mx-auto">
Expand Down Expand Up @@ -406,7 +381,7 @@ export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) {
</div>

{/* Inscripciones List */}
{loading ? (
{isInitialLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 size={32} className="animate-spin text-blue-500" />
</div>
Expand Down Expand Up @@ -481,7 +456,7 @@ export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) {
</div>
)}

{filteredInscripciones.length === 0 && !loading && (
{filteredInscripciones.length === 0 && !isInitialLoading && (
<div className="text-center py-8 md:py-12">
<AlertCircle size={32} className="mx-auto text-stone-500 mb-2 md:mb-4" />
<p className="text-xs md:text-sm text-stone-400">
Expand Down
36 changes: 35 additions & 1 deletion apps/next-frontend/hooks/use-colaborador-eventos.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -18,3 +25,30 @@ export function useGetMyColaboradorEvents() {
},
});
}

export function useUseColaboradorInviteCode() {
const queryClient = useQueryClient();

return useMutation<UseInviteCodeResponse, Error, string>({
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'] });
},
});
}
Loading
Loading