Phase 8: Visitor Management - Visitor/VisitorLog/AccessZone models - Check-in/out with badge generation - Zone occupancy tracking - Kiosk and management pages Phase 9: Messaging & Communication - Announcements with priority levels - Acknowledgement tracking - Shift notes for team handoffs - AnnouncementBanner component Phase 10: Compliance & Audit Trail - Immutable AuditLog model - Document versioning and approval workflow - Acknowledgement tracking for SOPs - CSV export for audit logs Phase 11: Accessibility & i18n - WCAG 2.1 AA compliance utilities - react-i18next with EN/ES translations - User preferences context (theme, font size, etc) - High contrast and reduced motion support Phase 12: Hardware Integration - QR code generation for batches/plants/visitors - Printable label system - Visitor badge printing Phase 13: Advanced Features - Environmental monitoring (sensors, readings, alerts) - Financial tracking (transactions, P&L reports) - AI/ML insights (yield predictions, anomaly detection)
287 lines
15 KiB
TypeScript
287 lines
15 KiB
TypeScript
import React from 'react';
|
|
import { Outlet, Link, useLocation } from 'react-router-dom';
|
|
import { useAuth } from '../context/AuthContext';
|
|
import ThemeToggle from './ThemeToggle';
|
|
import { CommandPalette } from './ui/CommandPalette';
|
|
import { SessionTimeoutWarning } from './ui/SessionTimeoutWarning';
|
|
import { PageTitleUpdater } from '../hooks/usePageTitle';
|
|
import AnnouncementBanner from './AnnouncementBanner';
|
|
import {
|
|
LayoutDashboard,
|
|
CheckSquare,
|
|
Home,
|
|
Sprout,
|
|
Clock,
|
|
LogOut,
|
|
Menu,
|
|
X,
|
|
User,
|
|
ChevronDown,
|
|
Package,
|
|
CalendarDays,
|
|
Shield,
|
|
Settings,
|
|
Fingerprint,
|
|
BarChart3,
|
|
Thermometer,
|
|
DollarSign,
|
|
Brain,
|
|
Users
|
|
} from 'lucide-react';
|
|
|
|
export default function Layout() {
|
|
const { user, logout } = useAuth();
|
|
const location = useLocation();
|
|
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
|
|
const [userMenuOpen, setUserMenuOpen] = React.useState(false);
|
|
|
|
const navItems = [
|
|
{ label: 'Dashboard', path: '/', icon: LayoutDashboard },
|
|
{ label: 'Walkthrough', path: '/walkthrough', icon: CheckSquare },
|
|
{ label: 'IPM', path: '/ipm', icon: Shield },
|
|
{ label: 'Tasks', path: '/tasks', icon: CalendarDays },
|
|
{ label: 'Reports', path: '/reports', icon: BarChart3 },
|
|
{ label: 'Inventory', path: '/supplies', icon: Package },
|
|
// Secondary items available in sidebar/more menu
|
|
{ label: 'Time', path: '/timeclock', icon: Clock },
|
|
{ label: 'Rooms', path: '/rooms', icon: Home },
|
|
{ label: 'Batches', path: '/batches', icon: Sprout },
|
|
{ label: 'Quick Actions', path: '/touch-points', icon: Fingerprint },
|
|
{ label: 'Layout', path: '/layout-designer', icon: LayoutDashboard },
|
|
{ label: 'Visitors', path: '/visitors', icon: Users },
|
|
{ label: 'Environment', path: '/environment', icon: Thermometer },
|
|
{ label: 'Financial', path: '/financial', icon: DollarSign },
|
|
{ label: 'AI Insights', path: '/insights', icon: Brain },
|
|
{ label: 'Roles', path: '/roles', icon: Shield },
|
|
{ label: 'Settings', path: '/settings', icon: Settings },
|
|
];
|
|
|
|
return (
|
|
<div className="flex flex-col h-screen bg-slate-50 dark:bg-slate-900">
|
|
{/* Skip to main content link (accessibility) */}
|
|
<a href="#main-content" className="skip-to-main">
|
|
Skip to main content
|
|
</a>
|
|
|
|
{/* Mobile Top Bar */}
|
|
<header className="md:hidden bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-4 py-3 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<img
|
|
src="/assets/logo-777-wolfpack.jpg"
|
|
alt="777 Wolfpack"
|
|
className="w-10 h-10 rounded-full ring-2 ring-emerald-500/20"
|
|
/>
|
|
<div>
|
|
<h1 className="text-sm font-bold text-slate-900 dark:text-white">
|
|
777 WOLFPACK
|
|
</h1>
|
|
<p className="text-xs text-slate-600 dark:text-slate-400">
|
|
{user?.name || user?.email}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700"
|
|
aria-label="Menu"
|
|
>
|
|
{mobileMenuOpen ? (
|
|
<X className="w-6 h-6 text-slate-700 dark:text-slate-300" />
|
|
) : (
|
|
<Menu className="w-6 h-6 text-slate-700 dark:text-slate-300" />
|
|
)}
|
|
</button>
|
|
</header>
|
|
|
|
{/* Mobile Menu Overlay */}
|
|
{mobileMenuOpen && (
|
|
<div className="md:hidden fixed inset-0 z-40 bg-black/50" onClick={() => setMobileMenuOpen(false)}>
|
|
<div className="absolute top-14 right-0 left-0 bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 p-4 space-y-2 animate-slide-in shadow-xl max-h-[80vh] overflow-y-auto">
|
|
<ThemeToggle />
|
|
|
|
<div className="border-t border-slate-100 dark:border-slate-700 my-2 pt-2">
|
|
<p className="text-xs font-semibold text-slate-500 uppercase px-2 mb-2">Navigation</p>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{navItems.map((item) => {
|
|
const Icon = item.icon;
|
|
const isActive = location.pathname === item.path;
|
|
return (
|
|
<Link
|
|
key={item.path}
|
|
to={item.path}
|
|
onClick={() => setMobileMenuOpen(false)}
|
|
className={`flex items-center gap-2 p-2 rounded-lg text-sm ${isActive
|
|
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-400 font-medium'
|
|
: 'text-slate-600 dark:text-slate-300'}`}
|
|
>
|
|
<Icon size={18} />
|
|
{item.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={logout}
|
|
className="w-full flex items-center justify-center gap-2 py-3 px-4 bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/40 text-red-700 dark:text-red-400 text-sm font-medium rounded-lg mt-4"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
Sign Out
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* Desktop Sidebar */}
|
|
<aside
|
|
className="hidden md:flex w-64 bg-white dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700 flex-col shadow-lg"
|
|
role="navigation"
|
|
aria-label="Main navigation"
|
|
>
|
|
<div className="p-6 border-b border-slate-200 dark:border-slate-700">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="relative">
|
|
<img
|
|
src="/assets/logo-777-wolfpack.jpg"
|
|
alt="777 Wolfpack"
|
|
className="w-12 h-12 rounded-full ring-2 ring-emerald-500/20"
|
|
/>
|
|
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-emerald-500 rounded-full border-2 border-white dark:border-slate-800 animate-pulse" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-lg font-bold text-slate-900 dark:text-white">
|
|
777 WOLFPACK
|
|
</h1>
|
|
<p className="text-xs text-slate-600 dark:text-slate-400">
|
|
Grow Ops Manager
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<ThemeToggle />
|
|
</div>
|
|
|
|
<nav className="flex-1 p-4 space-y-1 overflow-y-auto custom-scrollbar">
|
|
{navItems.map((item) => {
|
|
const Icon = item.icon;
|
|
const isActive = location.pathname === item.path;
|
|
|
|
return (
|
|
<Link
|
|
key={item.path}
|
|
to={item.path}
|
|
className={`group flex items-center gap-3 px-4 py-3 rounded-lg transition-all ${isActive
|
|
? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 font-semibold shadow-sm'
|
|
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50'
|
|
}`}
|
|
aria-current={isActive ? 'page' : undefined}
|
|
>
|
|
<Icon
|
|
className={`w-5 h-5 transition-transform ${isActive ? 'scale-110' : 'group-hover:scale-105'
|
|
}`}
|
|
strokeWidth={isActive ? 2.5 : 2}
|
|
/>
|
|
<span>{item.label}</span>
|
|
{isActive && (
|
|
<div className="ml-auto w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
|
)}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{/* User Menu (Desktop) */}
|
|
<div className="p-4 border-t border-slate-200 dark:border-slate-700">
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
|
className="w-full flex items-center gap-3 px-2 py-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors"
|
|
>
|
|
<div className="relative w-10 h-10 rounded-full bg-gradient-to-br from-emerald-600 to-emerald-700 flex items-center justify-center text-sm font-bold text-white ring-2 ring-emerald-500/20">
|
|
{user?.email[0].toUpperCase()}
|
|
<div className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-white dark:border-slate-800" />
|
|
</div>
|
|
<div className="flex-1 text-left overflow-hidden">
|
|
<p className="text-sm font-medium text-slate-900 dark:text-white truncate">
|
|
{user?.name || user?.email}
|
|
</p>
|
|
<p className="text-xs text-slate-600 dark:text-slate-400 uppercase">
|
|
{user?.role}
|
|
</p>
|
|
</div>
|
|
<ChevronDown className={`w-4 h-4 text-slate-400 transition-transform ${userMenuOpen ? 'rotate-180' : ''}`} />
|
|
</button>
|
|
|
|
{/* Dropdown Menu */}
|
|
{userMenuOpen && (
|
|
<div className="absolute bottom-full left-0 right-0 mb-2 bg-white dark:bg-slate-700 rounded-lg shadow-lg border border-slate-200 dark:border-slate-600 py-1 animate-scale-in">
|
|
<button
|
|
onClick={logout}
|
|
className="w-full flex items-center gap-2 px-4 py-2 text-sm text-red-700 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
Sign Out
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<main
|
|
id="main-content"
|
|
className="flex-1 overflow-auto pb-20 md:pb-8 custom-scrollbar"
|
|
role="main"
|
|
>
|
|
<PageTitleUpdater />
|
|
<AnnouncementBanner />
|
|
<div className="p-4 md:p-8">
|
|
<Outlet />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
{/* Mobile Bottom Navigation */}
|
|
<nav className="md:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700 safe-area-inset-bottom z-50">
|
|
{/* Visual Hint: Gradient Masks to indicate scrolling */}
|
|
<div className="absolute inset-y-0 left-0 w-4 bg-gradient-to-r from-white dark:from-slate-800 to-transparent pointer-events-none z-10" />
|
|
<div className="absolute inset-y-0 right-0 w-4 bg-gradient-to-l from-white dark:from-slate-800 to-transparent pointer-events-none z-10" />
|
|
|
|
<div className="flex gap-2 px-4 py-2 overflow-x-auto no-scrollbar snap-x snap-mandatory overscroll-x-contain">
|
|
{navItems.map((item) => {
|
|
const Icon = item.icon;
|
|
const isActive = location.pathname === item.path;
|
|
|
|
return (
|
|
<Link
|
|
key={item.path}
|
|
to={item.path}
|
|
className={`snap-center flex-shrink-0 flex flex-col items-center justify-center gap-1 w-[72px] py-1 rounded-xl transition-all ${isActive
|
|
? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400'
|
|
: 'text-slate-500 dark:text-slate-400'
|
|
}`}
|
|
aria-current={isActive ? 'page' : undefined}
|
|
>
|
|
<Icon
|
|
className={`w-6 h-6 transition-transform ${isActive ? 'scale-110' : ''}`}
|
|
strokeWidth={isActive ? 2.5 : 2}
|
|
/>
|
|
<span className={`text-[10px] truncate w-full text-center ${isActive ? 'font-semibold' : 'font-medium'}`}>
|
|
{item.label.replace('Walkthrough', 'Daily').replace('Quick Actions', 'Quick')}
|
|
</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</nav>
|
|
{/* Command Palette */}
|
|
<CommandPalette />
|
|
|
|
{/* Session Timeout Warning */}
|
|
<SessionTimeoutWarning />
|
|
</div>
|
|
);
|
|
}
|