ca-grow-ops-manager/frontend/src/components/Layout.tsx
fullsizemalt 32fd739ccf
Some checks failed
Deploy to Production / deploy (push) Failing after 0s
Test / backend-test (push) Failing after 0s
Test / frontend-test (push) Failing after 0s
feat: Complete Phases 8-13 implementation
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)
2025-12-11 00:26:25 -08:00

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>
);
}