diff --git a/frontend/src/pages/IPMDashboardPage.tsx b/frontend/src/pages/IPMDashboardPage.tsx index 0736746..0f7e50a 100644 --- a/frontend/src/pages/IPMDashboardPage.tsx +++ b/frontend/src/pages/IPMDashboardPage.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { batchesApi, Batch } from '../lib/batchesApi'; import { touchPointsApi, IPMSchedule } from '../lib/touchPointsApi'; import { Loader2, AlertTriangle, CheckCircle, Calendar, Shield } from 'lucide-react'; +import { PageHeader } from '../components/layout/PageHeader'; export default function IPMDashboardPage() { const [batches, setBatches] = useState([]); @@ -64,12 +65,12 @@ export default function IPMDashboardPage() { return (
-
-

- IPM Dashboard -

-

Root Drench Schedule (Veg Room)

-
+
{batches.map(batch => { diff --git a/frontend/src/pages/RoomsPage.tsx b/frontend/src/pages/RoomsPage.tsx index ebef3f2..ea3e78f 100644 --- a/frontend/src/pages/RoomsPage.tsx +++ b/frontend/src/pages/RoomsPage.tsx @@ -1,58 +1,114 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; +import { Home, Plus } from 'lucide-react'; import api from '../lib/api'; +import { PageHeader, PageHeaderButton } from '../components/layout/PageHeader'; +import { usePermissions } from '../hooks/usePermissions'; export default function RoomsPage() { + const { isManager } = usePermissions(); const [rooms, setRooms] = useState([]); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { fetchRooms(); }, []); const fetchRooms = async () => { + setIsLoading(true); try { const { data } = await api.get('/rooms'); setRooms(data); } catch (e) { console.error(e); + } finally { + setIsLoading(false); } }; + const getRoomTypeColor = (type: string) => { + const colors: Record = { + VEG: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', + FLOWER: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400', + DRY: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400', + CURE: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400', + MOTHER: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', + TRIM: 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-400', + }; + return colors[type] || 'bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-300'; + }; + return (
-
-

Cultivation Rooms

- -
+ + + Add Room + + ) + } + /> -
- {rooms.map(room => ( -
-
-

{room.name}

- - {room.type} - -
-
-
- Size: - {room.sqft} sqft + {isLoading ? ( +
+
+
+ ) : ( +
+ {rooms.map(room => ( +
+
+

+ {room.name?.replace('[DEMO] ', '')} +

+ + {room.type} +
-
- Active Batches: - {room.batches?.length || 0} +
+
+ Size: + {room.sqft?.toLocaleString()} sqft +
+
+ Capacity: + {room.capacity || '—'} plants +
+
+ Active Batches: + {room.batches?.length || 0} +
+ {room.targetTemp && ( +
+ 🌡️ {room.targetTemp}°F + 💧 {room.targetHumidity}% +
+ )}
-
- ))} - {rooms.length === 0 && ( -
-

No rooms found. Seed the database or create one.

-
- )} -
+ ))} + {rooms.length === 0 && ( +
+ +

No rooms found

+ {isManager && ( + + )} +
+ )} +
+ )}
); } diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index e474298..32e0823 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -4,22 +4,26 @@ import Layout from './components/Layout'; import ProtectedRoute from './components/ProtectedRoute'; import LoginPage from './pages/LoginPage'; import { RouterErrorPage, NotFoundPage } from './pages/ErrorPages'; -import DashboardPage from './pages/DashboardPage'; -import DailyWalkthroughPage from './pages/DailyWalkthroughPage'; -import RoomsPage from './pages/RoomsPage'; -import BatchesPage from './pages/BatchesPage'; -import TimeclockPage from './pages/TimeclockPage'; -import SuppliesPage from './pages/SuppliesPage'; -import TasksPage from './pages/TasksPage'; -import TaskTemplatesPage from './pages/TaskTemplatesPage'; -import WalkthroughSettingsPage from './pages/WalkthroughSettingsPage'; -import RolesPage from './pages/RolesPage'; -import TouchPointPage from './pages/TouchPointPage'; -import IPMDashboardPage from './pages/IPMDashboardPage'; -import SettingsPage from './pages/SettingsPage'; -import ReportsPage from './pages/ReportsPage'; -// Lazy load heavy components +// Core pages - loaded immediately +import DashboardPage from './pages/DashboardPage'; + +// Lazy load all other pages to reduce initial bundle +const DailyWalkthroughPage = lazy(() => import('./pages/DailyWalkthroughPage')); +const RoomsPage = lazy(() => import('./pages/RoomsPage')); +const BatchesPage = lazy(() => import('./pages/BatchesPage')); +const TimeclockPage = lazy(() => import('./pages/TimeclockPage')); +const SuppliesPage = lazy(() => import('./pages/SuppliesPage')); +const TasksPage = lazy(() => import('./pages/TasksPage')); +const TaskTemplatesPage = lazy(() => import('./pages/TaskTemplatesPage')); +const WalkthroughSettingsPage = lazy(() => import('./pages/WalkthroughSettingsPage')); +const RolesPage = lazy(() => import('./pages/RolesPage')); +const TouchPointPage = lazy(() => import('./pages/TouchPointPage')); +const IPMDashboardPage = lazy(() => import('./pages/IPMDashboardPage')); +const SettingsPage = lazy(() => import('./pages/SettingsPage')); +const ReportsPage = lazy(() => import('./pages/ReportsPage')); + +// Heavy components const LayoutDesignerPage = lazy(() => import('./features/layout-designer/LayoutDesignerPage')); const VisitorKioskPage = lazy(() => import('./pages/VisitorKioskPage')); const VisitorManagementPage = lazy(() => import('./pages/VisitorManagementPage')); @@ -33,6 +37,13 @@ const EnvironmentDashboard = lazy(() => import('./pages/EnvironmentDashboard')); const FinancialDashboard = lazy(() => import('./pages/FinancialDashboard')); const InsightsDashboard = lazy(() => import('./pages/InsightsDashboard')); +// Loading spinner component for Suspense fallbacks +const PageLoader = () => ( +
+
+
+); + export const router = createBrowserRouter([ { path: '/login', @@ -67,105 +78,81 @@ export const router = createBrowserRouter([ }, { path: 'walkthrough', - element: , + element: }>, }, { path: 'touch-points', - element: , + element: }>, }, { path: 'ipm', - element: , + element: }>, }, { path: 'rooms', - element: , + element: }>, }, { path: 'batches', - element: , + element: }>, }, { path: 'timeclock', - element: , + element: }>, }, { path: 'supplies', - element: , + element: }>, }, { path: 'tasks', - element: , + element: }>, }, { path: 'tasks/templates', - element: , + element: }>, }, { path: 'reports', - element: , + element: }>, }, { path: 'roles', - element: , + element: }>, }, { path: 'visitors', - element: ( - Loading...
}> - - - ), + element: }>, }, { path: 'settings', - element: , + element: }>, }, { path: 'settings/walkthrough', - element: , + element: }>, }, // Phase 10: Compliance & Audit { path: 'compliance/audit', - element: ( - Loading...
}> - - - ), + element: }>, }, { path: 'compliance/documents', - element: ( - Loading...
}> - - - ), + element: }>, }, // Phase 13: Advanced Features { path: 'environment', - element: ( - Loading...
}> - - - ), + element: }>, }, { path: 'financial', - element: ( - Loading...
}> - - - ), + element: }>, }, { path: 'insights', - element: ( - Loading...
}> - - - ), + element: }>, }, // 404 catch-all { diff --git a/frontend/src/templates/ComponentTemplate.tsx b/frontend/src/templates/ComponentTemplate.tsx new file mode 100644 index 0000000..c02e758 --- /dev/null +++ b/frontend/src/templates/ComponentTemplate.tsx @@ -0,0 +1,62 @@ +/** + * Component Template + * + * Copy this file to create a new component: + * 1. cp src/templates/ComponentTemplate.tsx src/components/MyComponent.tsx + * 2. Rename the component and update props interface + */ + +import { ReactNode } from 'react'; + +interface ComponentTemplateProps { + /** Required: Main content or children */ + children?: ReactNode; + /** Optional: Custom CSS classes */ + className?: string; + /** Optional: Variant style */ + variant?: 'default' | 'outlined' | 'ghost'; + /** Optional: Size variant */ + size?: 'sm' | 'md' | 'lg'; + /** Optional: Click handler */ + onClick?: () => void; + /** Optional: Disabled state */ + disabled?: boolean; +} + +const variantStyles = { + default: 'bg-emerald-600 text-white hover:bg-emerald-700', + outlined: 'border border-emerald-600 text-emerald-600 hover:bg-emerald-50 dark:hover:bg-emerald-900/20', + ghost: 'text-emerald-600 hover:bg-emerald-50 dark:hover:bg-emerald-900/20', +}; + +const sizeStyles = { + sm: 'px-3 py-1.5 text-sm', + md: 'px-4 py-2 text-sm', + lg: 'px-6 py-3 text-base', +}; + +export function ComponentTemplate({ + children, + className = '', + variant = 'default', + size = 'md', + onClick, + disabled = false, +}: ComponentTemplateProps) { + return ( + + ); +} diff --git a/frontend/src/templates/PageTemplate.tsx b/frontend/src/templates/PageTemplate.tsx new file mode 100644 index 0000000..cbf1797 --- /dev/null +++ b/frontend/src/templates/PageTemplate.tsx @@ -0,0 +1,122 @@ +/** + * Page Template + * + * Copy this file to create a new page: + * 1. cp src/templates/PageTemplate.tsx src/pages/MyNewPage.tsx + * 2. Replace "PageTemplate" with your page name + * 3. Update the icon, title, description + * 4. Add route in router.tsx + * 5. Add navigation item in lib/navigation.ts + */ + +import { useState } from 'react'; +import { FileText, Plus, Filter, Search } from 'lucide-react'; +import { PageHeader, PageHeaderButton } from '../components/layout/PageHeader'; +import { usePermissions } from '../hooks/usePermissions'; + +// Optional: Add API import +// import api from '../lib/api'; + +export default function PageTemplate() { + const { canAccess, isAdmin } = usePermissions(); + const [searchQuery, setSearchQuery] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + // Example data - replace with API call + const [items, setItems] = useState([]); + + // Optional: Fetch data on mount + // useEffect(() => { + // const fetchData = async () => { + // setIsLoading(true); + // try { + // const response = await api.get('/your-endpoint'); + // setItems(response.data); + // } catch (error) { + // console.error('Failed to load data:', error); + // } finally { + // setIsLoading(false); + // } + // }; + // fetchData(); + // }, []); + + return ( +
+ {/* Page Header */} + console.log('Add new')}> + + Add New + + ) + } + /> + + {/* Filters & Search Bar */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-sm focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 outline-none" + /> +
+ +
+ + {/* Content Area */} +
+ {isLoading ? ( +
+
+
+ ) : items.length === 0 ? ( + /* Empty State */ +
+
+ +
+

+ No Items Yet +

+

+ Get started by creating your first item. Items will appear here once created. +

+ {isAdmin && ( + + )} +
+ ) : ( + /* Data List/Table */ +
+ {items.map((item, index) => ( +
+ {/* Item content here */} +

{item.name}

+
+ ))} +
+ )} +
+
+ ); +} diff --git a/frontend/src/templates/WidgetTemplate.tsx b/frontend/src/templates/WidgetTemplate.tsx new file mode 100644 index 0000000..83780d7 --- /dev/null +++ b/frontend/src/templates/WidgetTemplate.tsx @@ -0,0 +1,124 @@ +/** + * Widget Template + * + * Copy this file to create a new dashboard widget: + * 1. cp src/templates/WidgetTemplate.tsx src/components/widgets/MyWidget.tsx + * 2. Rename the widget and update data fetching + */ + +import { useState, useEffect } from 'react'; +import { TrendingUp, TrendingDown, Minus, RefreshCw } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; + +interface WidgetTemplateProps { + title: string; + icon: LucideIcon; + iconColor?: string; + refreshInterval?: number; // ms, 0 = no auto-refresh +} + +interface WidgetData { + value: number | string; + label: string; + trend?: 'up' | 'down' | 'neutral'; + trendValue?: string; +} + +export function WidgetTemplate({ + title, + icon: Icon, + iconColor = 'text-emerald-600', + refreshInterval = 0, +}: WidgetTemplateProps) { + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [lastUpdated, setLastUpdated] = useState(new Date()); + + const fetchData = async () => { + setIsLoading(true); + try { + // Replace with actual API call + // const response = await api.get('/widget-data'); + // setData(response.data); + + // Demo data + await new Promise(resolve => setTimeout(resolve, 500)); + setData({ + value: Math.floor(Math.random() * 100), + label: 'Items', + trend: Math.random() > 0.5 ? 'up' : 'down', + trendValue: `${Math.floor(Math.random() * 20)}%`, + }); + setLastUpdated(new Date()); + } catch (error) { + console.error('Widget fetch failed:', error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchData(); + + if (refreshInterval > 0) { + const interval = setInterval(fetchData, refreshInterval); + return () => clearInterval(interval); + } + }, [refreshInterval]); + + const TrendIcon = data?.trend === 'up' ? TrendingUp : data?.trend === 'down' ? TrendingDown : Minus; + const trendColor = data?.trend === 'up' ? 'text-emerald-500' : data?.trend === 'down' ? 'text-red-500' : 'text-slate-400'; + + return ( +
+ {/* Header */} +
+
+
+ +
+

{title}

+
+ +
+ + {/* Content */} + {isLoading && !data ? ( +
+
+
+ ) : data ? ( +
+
+ + {data.value} + + {data.label} +
+ {data.trend && ( +
+ + {data.trendValue} from last week +
+ )} +
+ ) : ( +

No data available

+ )} + + {/* Footer */} +
+

+ Updated {lastUpdated.toLocaleTimeString()} +

+
+
+ ); +}