import { useState, useEffect, useRef } from 'react'; import { QRCodeSVG as QRCode } from 'qrcode.react'; import { User, Building, Clock, CheckCircle, XCircle, UserPlus, LogOut, Search, Shield, AlertTriangle, Camera, Trash2 } from 'lucide-react'; import { visitorsApi, Visitor, ActiveVisitor } from '../lib/visitorsApi'; type KioskMode = 'home' | 'new-visitor' | 'returning' | 'check-in' | 'check-out' | 'success'; export default function VisitorKioskPage() { const [mode, setMode] = useState('home'); const [activeVisitors, setActiveVisitors] = useState([]); const [searchResults, setSearchResults] = useState([]); const [selectedVisitor, setSelectedVisitor] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [successData, setSuccessData] = useState<{ badgeNumber?: string; message: string; visitId?: string } | null>(null); // Form state for new visitor const [formData, setFormData] = useState({ name: '', company: '', email: '', phone: '', type: 'VISITOR' as const, purpose: '', ndaAccepted: false }); // Photo capture state const [capturedPhoto, setCapturedPhoto] = useState(null); const [showCamera, setShowCamera] = useState(false); const videoRef = useRef(null); const streamRef = useRef(null); const startCamera = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: 640, height: 480 } }); streamRef.current = stream; if (videoRef.current) { videoRef.current.srcObject = stream; } setShowCamera(true); } catch (err) { console.error('Camera access denied:', err); setError('Camera access denied. Please allow camera permissions.'); } }; const capturePhoto = () => { if (videoRef.current) { const canvas = document.createElement('canvas'); canvas.width = videoRef.current.videoWidth; canvas.height = videoRef.current.videoHeight; const ctx = canvas.getContext('2d'); if (ctx) { ctx.drawImage(videoRef.current, 0, 0); const photoDataUrl = canvas.toDataURL('image/jpeg', 0.8); setCapturedPhoto(photoDataUrl); stopCamera(); } } }; const stopCamera = () => { if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()); streamRef.current = null; } setShowCamera(false); }; const retakePhoto = () => { setCapturedPhoto(null); startCamera(); }; useEffect(() => { loadActiveVisitors(); }, []); const loadActiveVisitors = async () => { try { const { visitors } = await visitorsApi.getActive(); setActiveVisitors(visitors); } catch (err) { console.error('Failed to load active visitors:', err); } }; const handleSearch = async () => { if (!searchQuery.trim()) return; setLoading(true); try { const { visitors } = await visitorsApi.getAll({ search: searchQuery }); setSearchResults(visitors); } catch (err) { setError('Search failed'); } finally { setLoading(false); } }; const handleNewVisitorSubmit = async () => { if (!formData.name || !formData.purpose) { setError('Name and purpose are required'); return; } setLoading(true); setError(null); try { // Only include non-empty fields to avoid validation errors on optional fields const visitorData: Record = { name: formData.name, purpose: formData.purpose, type: formData.type }; if (formData.company) visitorData.company = formData.company; if (formData.email) visitorData.email = formData.email; if (formData.phone) visitorData.phone = formData.phone; if (capturedPhoto) visitorData.photoUrl = capturedPhoto; const visitor = await visitorsApi.create(visitorData); const result = await visitorsApi.checkIn(visitor.id, { ndaAccepted: formData.ndaAccepted }); setSuccessData({ badgeNumber: result.badgeNumber, visitId: result.visitId, message: `Welcome, ${visitor.name}!` }); setMode('success'); loadActiveVisitors(); } catch (err: any) { setError(err.response?.data?.error || 'Failed to check in'); } finally { setLoading(false); } }; const handleReturningVisitorCheckIn = async (visitor: Visitor) => { setLoading(true); setError(null); try { const result = await visitorsApi.checkIn(visitor.id, {}); setSuccessData({ badgeNumber: result.badgeNumber, visitId: result.visitId, message: `Welcome back, ${visitor.name}!` }); setMode('success'); loadActiveVisitors(); } catch (err: any) { setError(err.response?.data?.error || 'Failed to check in'); } finally { setLoading(false); } }; const handleCheckOut = async (visitor: ActiveVisitor) => { setLoading(true); setError(null); try { const result = await visitorsApi.checkOut(visitor.visitorId); setSuccessData({ message: `Goodbye, ${visitor.name}! Visit duration: ${result.duration} minutes` }); setMode('success'); loadActiveVisitors(); } catch (err: any) { setError(err.response?.data?.error || 'Failed to check out'); } finally { setLoading(false); } }; const resetToHome = () => { setMode('home'); setSelectedVisitor(null); setSearchQuery(''); setSearchResults([]); setError(null); setSuccessData(null); setFormData({ name: '', company: '', email: '', phone: '', type: 'VISITOR', purpose: '', ndaAccepted: false }); setCapturedPhoto(null); stopCamera(); }; // Auto-reset success screen after 5 seconds ONLY for check-out (no badge number) useEffect(() => { if (mode === 'success' && !successData?.badgeNumber) { const timer = setTimeout(resetToHome, 5000); return () => clearTimeout(timer); } }, [mode, successData]); return (
{/* Header */}

Visitor Check-In

777 Wolfpack Facility

{new Date().toLocaleTimeString()}
{/* Main Content */}
{/* Error Alert */} {error && (
{error}
)} {/* Home Screen */} {mode === 'home' && (

Welcome

Please select an option to continue

{/* Check Out Section */} {activeVisitors.length > 0 && (

Check Out ({activeVisitors.length} visitors on-site)

{activeVisitors.map(visitor => ( ))}
)}
)} {/* New Visitor Form */} {mode === 'new-visitor' && (

New Visitor Registration

setFormData({ ...formData, name: e.target.value })} className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white text-lg focus:border-emerald-500 outline-none" placeholder="John Smith" />
setFormData({ ...formData, company: e.target.value })} className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white focus:border-emerald-500 outline-none" />
setFormData({ ...formData, purpose: e.target.value })} className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white focus:border-emerald-500 outline-none" placeholder="Meeting with..." />
{/* Photo Capture Section */}
{!capturedPhoto && !showCamera && ( )} {showCamera && (
)} {capturedPhoto && (
Captured
Photo Captured
)}
setFormData({ ...formData, email: e.target.value })} className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white focus:border-emerald-500 outline-none" />
setFormData({ ...formData, phone: e.target.value })} className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white focus:border-emerald-500 outline-none" />
)} {/* Returning Visitor Search */} {mode === 'returning' && (

Find Your Record

setSearchQuery(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSearch()} className="w-full bg-slate-800 border border-slate-700 rounded-xl pl-12 pr-4 py-3 text-white text-lg focus:border-emerald-500 outline-none" placeholder="Search by name, email, or company..." />
{searchResults.length > 0 && (
{searchResults.map(visitor => ( ))}
)} {searchQuery && searchResults.length === 0 && !loading && (
No visitors found. Try a different search or register as a new visitor.
)}
)} {/* Success Screen */} {mode === 'success' && successData && (

{successData.message}

{successData.badgeNumber && ( // Only show QR for Check-IN, not Check-OUT

Scan for Digital Badge

Use your phone to carry your badge with you.

Badge Number

{successData.badgeNumber}

)} {!successData.badgeNumber && ( // Auto-redirect for Check-OUT only

Returning to home in 5 seconds...

)}
)}
{/* Footer */}
); }