From ae92d560ee8af48f65c92b8bdc4a9a952765617a Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:36:23 -0800 Subject: [PATCH] feat(ui): Refactor Supplies and Visitor Management pages - Implemented DataTable for high-density inventory tracking - Modernized Visitor Management with Live Manifest and Access Logs - Applied 777 Wolfpack theme and motion transitions - Standardized metrics with MetricCard and StatusBadge --- frontend/src/pages/SuppliesPage.tsx | 663 ++++++++------- frontend/src/pages/VisitorManagementPage.tsx | 799 ++++++++++--------- 2 files changed, 804 insertions(+), 658 deletions(-) diff --git a/frontend/src/pages/SuppliesPage.tsx b/frontend/src/pages/SuppliesPage.tsx index 855e87e..813dea9 100644 --- a/frontend/src/pages/SuppliesPage.tsx +++ b/frontend/src/pages/SuppliesPage.tsx @@ -1,11 +1,14 @@ import { useState, useEffect } from 'react'; import { Plus, Search, ShoppingCart, Package, Filter, - AlertCircle, ExternalLink, X, Loader2 + AlertCircle, ExternalLink, X, Loader2, Minus, MoreVertical } from 'lucide-react'; import { suppliesApi, SupplyItem, SupplyCategory } from '../lib/suppliesApi'; -import { PageHeader, EmptyState, CardSkeleton } from '../components/ui/LinearPrimitives'; +import { PageHeader, EmptyState, StatusBadge } 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'; export default function SuppliesPage() { const { addToast } = useToast(); @@ -39,12 +42,14 @@ export default function SuppliesPage() { setItems(data); } catch (error) { console.error('Failed to load supplies', error); + addToast('Failed to load inventory', 'error'); } finally { setLoading(false); } }; const handleQuantityAdjust = async (id: string, adjustment: number) => { + // Optimistic update setItems(current => current.map(item => { if (item.id === id) { return { ...item, quantity: Math.max(0, item.quantity + adjustment) }; @@ -55,6 +60,7 @@ export default function SuppliesPage() { try { await suppliesApi.adjustQuantity(id, adjustment); } catch (error) { + addToast('Failed to sync quantity', 'error'); loadItems(); } }; @@ -81,7 +87,7 @@ export default function SuppliesPage() { setItems(current => current.map(item => item.id === id ? updated : item)); addToast('Marked as ordered', 'success'); } catch (error) { - addToast('Failed to update', 'error'); + addToast('Failed to update order status', 'error'); } }; @@ -98,304 +104,397 @@ export default function SuppliesPage() { const categories: SupplyCategory[] = ['FILTER', 'CLEANING', 'PPE', 'OFFICE', 'BATHROOM', 'KITCHEN', 'MAINTENANCE', 'OTHER']; + const columns: Column[] = [ + { + key: 'name', + header: 'Item Name', + cell: (item) => ( +
+ + {item.name} + + {item.quantity <= item.minThreshold && ( + + )} +
+ ) + }, + { + key: 'category', + header: 'Category', + cell: (item) => ( + + ), + hideOnMobile: true + }, + { + key: 'location', + header: 'Location', + cell: (item) => {item.location || 'โ€”'}, + hideOnMobile: true + }, + { + key: 'quantity', + header: 'Quantity', + className: 'w-48', + cell: (item) => ( +
+
+ +
+ {item.quantity} +
+ +
+ {item.unit} +
+ ) + }, + { + key: 'minThreshold', + header: 'Min', + cell: (item) => ( +
+ Min: {item.minThreshold} +
+ ), + hideOnMobile: true + }, + { + key: 'actions', + header: '', + className: 'text-right', + cell: (item) => ( +
+ {item.productUrl && ( + e.stopPropagation()} + className="p-2 text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-500/10 rounded-lg transition-colors" + title="Buy Now" + > + + + )} + {item.quantity <= item.minThreshold && ( + + )} +
+ ) + } + ]; + return ( -
+
setIsAddModalOpen(true)} - className="btn btn-primary" + className="group relative bg-indigo-600 hover:bg-indigo-500 text-white px-5 py-2.5 rounded-xl font-bold text-sm tracking-tight transition-all active:scale-95 shadow-xl shadow-indigo-600/10 flex items-center gap-2" > - - Add Item + + Add Supply Record } /> - {/* View Toggle */} -
- + {/* Dashboard Ribbon */} +
+
+
+

Stock Health

+

{items.length} Unique SKUs

+
+ +
-
- - {/* Filters */} -
-
- - setSearch(e.target.value)} - className="input w-full pl-9" - /> -
-
- - {categories.map(cat => ( - - ))} -
-
- - {/* Items */} - {loading ? ( -
- {Array.from({ length: 6 }).map((_, i) => )} -
- ) : filteredItems.length === 0 ? ( - { setSearch(''); setCategoryFilter('ALL'); }} - className="text-accent hover:underline text-sm" - > - Clear filters - - ) : ( - - ) - } - /> - ) : ( -
- {filteredItems.map(item => ( -
-
-
-

- {item.name} - {item.quantity <= item.minThreshold && ( - - )} -

-
- {item.category.toLowerCase()} - {item.location && ๐Ÿ“ {item.location}} -
-
-
-
{item.quantity}
-
{item.unit}
-
-
- -
-
- - -
- - {item.quantity <= item.minThreshold && ( -
- {item.productUrl && ( - - - Buy - - )} - -
- )} -
+
+
+
+ + setSearch(e.target.value)} + className="w-full bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-lg pl-9 pr-4 py-2 text-sm focus:ring-2 focus:ring-indigo-500/20 transition-all" + />
- ))} -
- )} - - {/* Mobile FAB */} - - - {/* Add Modal */} - {isAddModalOpen && ( -
-
-
-

Add New Item

- -
-
-
- - setNewItem({ ...newItem, name: e.target.value })} - placeholder="e.g. 5 Gallon Pots" - /> -
- -
-
- - -
-
- - setNewItem({ ...newItem, unit: e.target.value })} - placeholder="box, each..." - /> -
-
- -
-
- - setNewItem({ ...newItem, quantity: parseInt(e.target.value) || 0 })} - /> -
-
- - setNewItem({ ...newItem, minThreshold: parseInt(e.target.value) || 0 })} - /> -
-
- -
-
- - setNewItem({ ...newItem, vendor: e.target.value })} - placeholder="e.g. Amazon" - /> -
-
- - setNewItem({ ...newItem, productUrl: e.target.value })} - placeholder="https://..." - /> -
-
- -
- - setNewItem({ ...newItem, location: e.target.value })} - placeholder="e.g. Storage Room A" - /> -
- - -
- )} +
+ + {/* Controls & Table */} +
+
+
+ + +
+ +
+ + {categories.map(cat => ( + + ))} +
+
+ + { setSearch(''); setCategoryFilter('ALL'); }} + className="font-bold text-indigo-500 hover:text-indigo-600 uppercase text-xs tracking-widest" + > + Reset Filters + + ) : ( + + ) + } + /> + } + /> +
+ + {/* Add Modal */} + + {isAddModalOpen && ( +
+ setIsAddModalOpen(false)} + className="absolute inset-0 bg-black/60 backdrop-blur-sm" + /> + +
+

Add Supply Record

+ +
+ +
+
+
+ + setNewItem({ ...newItem, name: e.target.value })} + placeholder="e.g. 5 Gallon Nursery Pots" + /> +
+ +
+
+ + +
+
+ + setNewItem({ ...newItem, unit: e.target.value })} + placeholder="box, each, gal..." + /> +
+
+ +
+
+ + setNewItem({ ...newItem, quantity: parseInt(e.target.value) || 0 })} + /> +
+
+ + setNewItem({ ...newItem, minThreshold: parseInt(e.target.value) || 0 })} + /> +
+
+ +
+ + setNewItem({ ...newItem, location: e.target.value })} + placeholder="e.g. Warehouse Rack B4" + /> +
+ +
+
+ + setNewItem({ ...newItem, vendor: e.target.value })} + placeholder="Amazon, Home Depot..." + /> +
+
+ + setNewItem({ ...newItem, productUrl: e.target.value })} + placeholder="https://..." + /> +
+
+
+ + +
+
+
+ )} +
); } diff --git a/frontend/src/pages/VisitorManagementPage.tsx b/frontend/src/pages/VisitorManagementPage.tsx index 522c67b..0ad3217 100644 --- a/frontend/src/pages/VisitorManagementPage.tsx +++ b/frontend/src/pages/VisitorManagementPage.tsx @@ -1,12 +1,16 @@ import { useState, useEffect } from 'react'; import { Users, UserPlus, LogIn, LogOut, Building, Search, - Filter, Download, Calendar, Shield, AlertTriangle, X + Filter, Download, Calendar, Shield, AlertTriangle, X, + History, LayoutGrid, Camera, MapPin, Clock, MoreHorizontal } from 'lucide-react'; import { visitorsApi, Visitor, VisitorLog, ActiveVisitor, AccessZone } from '../lib/visitorsApi'; -import { PageHeader, MetricCard, EmptyState, CardSkeleton } from '../components/ui/LinearPrimitives'; +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'; @@ -39,6 +43,7 @@ export default function VisitorManagementPage() { } } catch (error) { console.error('Failed to load data:', error); + addToast('Failed to load records', 'error'); } finally { setLoading(false); } @@ -50,7 +55,7 @@ export default function VisitorManagementPage() { addToast(`${visitor.name} checked out`, 'success'); loadData(); } catch (error) { - addToast('Failed to check out', 'error'); + addToast('Failed to sign out visitor', 'error'); } }; @@ -66,7 +71,7 @@ export default function VisitorManagementPage() { try { await visitorsApi.revoke(revokeModal.visitor.visitorId, revokeModal.notes); setRevokeModal(null); - addToast('Access revoked', 'warning'); + addToast('Access credentials revoked', 'warning'); loadData(); } catch (error) { addToast('Failed to revoke access', 'error'); @@ -74,409 +79,451 @@ export default function VisitorManagementPage() { }; const getTypeBadge = (type: string) => { - const badges: Record = { - VISITOR: 'badge-accent', - CONTRACTOR: 'badge-warning', - INSPECTOR: 'badge-destructive', - VENDOR: 'badge-accent', - DELIVERY: 'badge-success', - OTHER: 'badge' + const variants: Record = { + VISITOR: 'active', + CONTRACTOR: 'warning', + INSPECTOR: 'error', + VENDOR: 'accent', + DELIVERY: 'default', + OTHER: 'default' }; - return badges[type] || 'badge'; + return ; }; - return ( -
- [] = [ + { + 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 ( + } /> - {/* Stats */} -
- - - - -
- - {/* Tabs */} -
- {(['active', 'all', 'zones', 'reports'] as TabType[]).map(tab => ( - - ))} -
- - {/* Gallery View */} - {activeTab === 'gallery' && ( -
-
-
- - setSearchQuery(e.target.value)} - onKeyDown={e => e.key === 'Enter' && loadData()} - placeholder="Search visitor badges..." - className="input w-full pl-9" - /> -
-
- - {loading ? ( -
- {Array.from({ length: 8 }).map((_, i) => )} -
- ) : allVisitors.length === 0 ? ( - + + + +
+
+ + setSearchQuery(e.target.value)} + onKeyDown={e => e.key === 'Enter' && loadData()} + className="w-full bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-lg pl-9 pr-4 py-2 text-sm" /> - ) : ( -
- {allVisitors.map(visitor => ( -
-
- {visitor.photoUrl ? ( +
+
+
+ + {/* 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} - ) : ( -
- - No Photo +
+
+

{visitor.name}

+

{visitor.type}

- )} - {/* Overlay Type Badge */} -
- - {visitor.type} -
-
-
-

{visitor.name}

-

{visitor.company || 'Private Visitor'}

-
- - {visitor.logs[0] ? new Date(visitor.logs[0].entryTime).toLocaleDateString() : 'N/A'} - - - {visitor.logs[0]?.status === 'CHECKED_IN' ? 'Active' : 'Offline'} - -
-
-
- ))} -
- )} -
- )} + )) + )} +
+ )} - {/* Active Visitors Tab */} - {activeTab === 'active' && ( -
- {loading ? ( -
- {Array.from({ length: 3 }).map((_, i) => )} -
- ) : activeVisitors.length === 0 ? ( - - ) : ( -
- {activeVisitors.map(visitor => ( -
-
-
- {visitor.name.charAt(0)} -
+ {activeTab === 'zones' && ( + + {zones.map(zone => ( +
-
- {visitor.name} - - {visitor.type} - -
-
- {visitor.company && `${visitor.company} ยท `} - {visitor.purpose} -
-
-
-
-
-
Badge
-
{visitor.badgeNumber}
-
-
-
Duration
-
{formatDuration(visitor.entryTime)}
-
-
- - -
-
-
- ))} -
- )} -
- )} - - {/* All Visitors Tab */} - {activeTab === 'all' && ( -
-
-
- - setSearchQuery(e.target.value)} - onKeyDown={e => e.key === 'Enter' && loadData()} - placeholder="Search all records..." - className="input w-full pl-9" - /> -
- -
- -
- - - - - - - - - - - - {allVisitors.map(visitor => ( - - - - - - - + + ))} - -
VisitorTypeCompanyLast VisitStatus
-
-
- {visitor.name.charAt(0)} -
-
-
{visitor.name}
-
{visitor.email}
+
+
+
+
-
- - {visitor.type} +

{zone.name}

+

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

+ + +
+
+ {[1, 2].map(i => ( +
+ ))} +
+ + {zone.maxOccupancy ? `Limit ${zone.maxOccupancy}` : 'No Limit'} -
{visitor.company || 'โ€”'} - {visitor.logs[0] - ? new Date(visitor.logs[0].entryTime).toLocaleDateString() - : 'Never' - } - - {visitor.logs[0]?.status === 'CHECKED_IN' ? ( - On-Site - ) : visitor.logs[0]?.status === 'REVOKED' ? ( - REVOKED - ) : ( - Off-Site - )} -
-
-
- )} + + + )} - {/* Zones Tab */} - {activeTab === 'zones' && ( -
- {zones.map(zone => ( -
-
-
-

{zone.name}

- {zone.code} -
- -
- {zone.description && ( -

{zone.description}

- )} -
- {zone.escortRequired && Escort Required} - {zone.ndaRequired && NDA Required} - {zone.maxOccupancy && Max: {zone.maxOccupancy}} -
-
- ))} - - -
- )} - - {/* Reports Tab */} - {activeTab === 'reports' && ( -
-
-

Visitor Reports

- -
- -
- -
-

Custom Report

-
-
- -
- - + {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 && ( -
-
-
-
- -

Revoke Access

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

Security Revocation

+
+
- -
-

- Revoke access for {revokeModal.visitor.name}? - This will invalidate their badge. -

+
+

+ 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. +

-
- -