import { useState, useEffect } from 'react'; import { Users, UserPlus, LogIn, LogOut, Building, Search, Filter, Download, Calendar, Shield, AlertTriangle, X, History, LayoutGrid, Camera, MapPin, Clock, MoreHorizontal, Plus } from 'lucide-react'; import { visitorsApi, Visitor, VisitorLog, ActiveVisitor, AccessZone } from '../lib/visitorsApi'; import { PageHeader, MetricCard, EmptyState, StatusBadge, ActionButton } from '../components/ui/LinearPrimitives'; import { DataTable, Column } from '../components/ui/DataTable'; import { useToast } from '../context/ToastContext'; import { cn } from '../lib/utils'; import { motion, AnimatePresence } from 'framer-motion'; type TabType = 'active' | 'all' | 'zones' | 'reports' | 'gallery'; export default function VisitorManagementPage() { const { addToast } = useToast(); const [activeTab, setActiveTab] = useState('active'); const [activeVisitors, setActiveVisitors] = useState<(ActiveVisitor & { id: string })[]>([]); const [allVisitors, setAllVisitors] = useState<(Visitor & { logs: VisitorLog[] })[]>([]); const [zones, setZones] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [revokeModal, setRevokeModal] = useState<{ visitor: ActiveVisitor & { id: string }, notes: string } | null>(null); useEffect(() => { loadData(); }, [activeTab]); const loadData = async () => { setLoading(true); try { if (activeTab === 'active') { const { visitors } = await visitorsApi.getActive(); setActiveVisitors(visitors.map(v => ({ ...v, id: v.logId }))); } else if (activeTab === 'all' || activeTab === 'gallery') { const { visitors } = await visitorsApi.getAll({ search: searchQuery || undefined }); setAllVisitors(visitors); } else if (activeTab === 'zones') { const zonesData = await visitorsApi.getZones(); setZones(zonesData); } } catch (error) { console.error('Failed to load data:', error); addToast('Failed to load records', 'error'); } finally { setLoading(false); } }; const handleCheckOut = async (visitor: ActiveVisitor) => { try { await visitorsApi.checkOut(visitor.visitorId); addToast(`${visitor.name} checked out`, 'success'); loadData(); } catch (error) { addToast('Failed to sign out visitor', 'error'); } }; const formatDuration = (entryTime: string) => { const minutes = Math.floor((Date.now() - new Date(entryTime).getTime()) / 60000); if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); return `${hours}h ${minutes % 60}m`; }; const handleRevoke = async () => { if (!revokeModal) return; try { await visitorsApi.revoke(revokeModal.visitor.visitorId, revokeModal.notes); setRevokeModal(null); addToast('Access credentials revoked', 'warning'); loadData(); } catch (error) { addToast('Failed to revoke access', 'error'); } }; const getTypeBadge = (type: string) => { const variants: Record = { VISITOR: 'active', CONTRACTOR: 'pending', INSPECTOR: 'error', VENDOR: 'default', DELIVERY: 'default', OTHER: 'default' }; return ; }; // Columns for Active (Live) Visitors const activeColumns: Column[] = [ { key: 'visitor', header: 'On-Site Personnel', cell: (v) => (
{v.name.charAt(0)}
{v.name}
{v.company || 'Private'}
) }, { key: 'type', header: 'Type', cell: (v) => getTypeBadge(v.type), hideOnMobile: true }, { key: 'badge', header: 'Badge ID', cell: (v) => {v.badgeNumber}, hideOnMobile: true }, { key: 'duration', header: 'Duration', cell: (v) => (
{formatDuration(v.entryTime)}
) }, { key: 'actions', header: '', className: 'text-right', cell: (v) => (
) } ]; // Columns for Visitor History const historyColumns: Column[] = [ { key: 'visitor', header: 'Visitor Information', cell: (v) => (
{v.name}
{v.email}
) }, { key: 'type', header: 'Role', cell: (v) => getTypeBadge(v.type), hideOnMobile: true }, { key: 'company', header: 'Entity', cell: (v) => {v.company || '—'}, hideOnMobile: true }, { key: 'lastVisit', header: 'Last Entry', cell: (v) => (
{v.logs[0] ? new Date(v.logs[0].entryTime).toLocaleDateString() : 'No history'}
) }, { key: 'status', header: 'System Status', cell: (v) => { const status = v.logs[0]?.status; if (status === 'CHECKED_IN') return ; if (status === 'REVOKED') return ; return ; }, hideOnMobile: true } ]; return ( } /> {/* Metrics Dashboard */}
setSearchQuery(e.target.value)} onKeyDown={e => e.key === 'Enter' && loadData()} className="w-full bg-white dark:bg-slate-950 border border-[var(--color-border-subtle)] rounded-lg pl-9 pr-4 py-2 text-sm" />
{/* Tabbed Interface */}
{[ { id: 'active', label: 'Live Manifest', icon: RadioNodeIcon }, { id: 'all', label: 'Access Logs', icon: History }, { id: 'gallery', label: 'Visual Archive', icon: Camera }, { id: 'zones', label: 'Zones & Gates', icon: Building }, { id: 'reports', label: 'Audits', icon: Shield } ].map(tab => ( ))}
{/* Content Views */}
{activeTab === 'active' && ( window.open('/kiosk', '_blank')} className="font-bold text-indigo-500 text-xs tracking-widest uppercase"> Check-In Personnel } /> } /> )} {activeTab === 'all' && ( } /> )} {activeTab === 'gallery' && ( {loading ? ( Array.from({ length: 12 }).map((_, i) => (
)) ) : allVisitors.filter(v => v.photoUrl).length === 0 ? (
) : ( allVisitors.filter(v => v.photoUrl).map(visitor => (
{visitor.name}

{visitor.name}

{visitor.type}

)) )} )} {activeTab === 'zones' && ( {zones.map(zone => (

{zone.name}

{zone.description || 'No zone constraints defined.'}

{[1, 2].map(i => (
))}
{zone.maxOccupancy ? `Limit ${zone.maxOccupancy}` : 'No Limit'}
))} )} {activeTab === 'reports' && (

Standard Audit Packages

{[ { label: 'Weekly Access Manifest', size: '2.4mb' }, { label: 'Compliance Incident Summary', size: '1.1mb' }, { label: 'Contractor Billing verification', size: '4.8mb' } ].map(r => ( ))}

Custom Data Weaver

Generate multi-dimensional reports across multiple facility zones and time periods.

)}
{/* Revoke Modal */} {revokeModal && (
setRevokeModal(null)} className="absolute inset-0 bg-black/60 backdrop-blur-sm" />

Security Revocation

You are about to revoke all facility access privileges for {revokeModal.visitor.name}. This action is immediate and will be logged in the permanent compliance record.