// ================================================================ // frontend/src/pages/app/DashboardPage.tsx // ================================================================ /** * JTBD-Aligned Dashboard Page * * Complete redesign based on Jobs To Be Done methodology. * Focused on answering: "What requires my attention right now?" * * Key principles: * - Automation-first (show what system did) * - Action-oriented (prioritize tasks) * - Progressive disclosure (show 20% that matters 80%) * - Mobile-first (one-handed operation) * - Trust-building (explain system reasoning) */ import { useState, useEffect, useMemo } from 'react'; import { Plus, Sparkles } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useTenant } from '../../stores/tenant.store'; import { useApprovePurchaseOrder, useStartProductionBatch, } from '../../api/hooks/useProfessionalDashboard'; import { useDashboardData, useDashboardRealtimeSync } from '../../api/hooks/useDashboardData'; import { useRejectPurchaseOrder } from '../../api/hooks/purchase-orders'; import { useIngredients } from '../../api/hooks/inventory'; import { useSuppliers } from '../../api/hooks/suppliers'; import { useRecipes } from '../../api/hooks/recipes'; import { useQualityTemplates } from '../../api/hooks/qualityTemplates'; import { SetupWizardBlocker } from '../../components/dashboard/SetupWizardBlocker'; import { CollapsibleSetupBanner } from '../../components/dashboard/CollapsibleSetupBanner'; import { SystemStatusBlock, PendingPurchasesBlock, PendingDeliveriesBlock, ProductionStatusBlock, } from '../../components/dashboard/blocks'; import { UnifiedPurchaseOrderModal } from '../../components/domain/procurement/UnifiedPurchaseOrderModal'; import { UnifiedAddWizard } from '../../components/domain/unified-wizard'; import type { ItemType } from '../../components/domain/unified-wizard'; import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding'; import { Package, Users, BookOpen, Shield } from 'lucide-react'; // Import Enterprise Dashboard import EnterpriseDashboardPage from './EnterpriseDashboardPage'; import { useSubscription } from '../../api/hooks/subscription'; import { SUBSCRIPTION_TIERS } from '../../api/types/subscription'; // Rename the existing component to BakeryDashboard export function BakeryDashboard() { const { t } = useTranslation(['dashboard', 'common', 'alerts']); const { currentTenant } = useTenant(); const tenantId = currentTenant?.id || ''; const { startTour } = useDemoTour(); const isDemoMode = localStorage.getItem('demo_mode') === 'true'; // Unified Add Wizard state const [isAddWizardOpen, setIsAddWizardOpen] = useState(false); // PO Details Modal state const [selectedPOId, setSelectedPOId] = useState(null); const [isPOModalOpen, setIsPOModalOpen] = useState(false); const [poModalMode, setPOModalMode] = useState<'view' | 'edit'>('view'); // Setup Progress Data - use localStorage as fallback during loading const setupProgressFromStorage = useMemo(() => { try { const cached = localStorage.getItem(`setup_progress_${tenantId}`); return cached ? parseInt(cached, 10) : 0; } catch { return 0; } }, [tenantId]); // Fetch setup data to determine true progress const { data: ingredients = [], isLoading: loadingIngredients } = useIngredients( tenantId, {}, { enabled: !!tenantId } ); const { data: suppliers = [], isLoading: loadingSuppliers } = useSuppliers( tenantId, {}, { enabled: !!tenantId } ); const { data: recipes = [], isLoading: loadingRecipes } = useRecipes( tenantId, {}, { enabled: !!tenantId } ); const { data: qualityData, isLoading: loadingQuality } = useQualityTemplates( tenantId, {}, { enabled: !!tenantId } ); const qualityTemplates = Array.isArray(qualityData?.templates) ? qualityData.templates : []; // NEW: Single unified data fetch for all 4 dashboard blocks const { data: dashboardData, isLoading: dashboardLoading, refetch: refetchDashboard, } = useDashboardData(tenantId); // Enable SSE real-time state synchronization useDashboardRealtimeSync(tenantId); // Mutations const approvePO = useApprovePurchaseOrder(); const rejectPO = useRejectPurchaseOrder(); const startBatch = useStartProductionBatch(); // Handlers const handleApprove = async (poId: string) => { try { await approvePO.mutateAsync({ tenantId, poId }); // SSE will handle refetch, but trigger immediate refetch for responsiveness refetchDashboard(); } catch (error) { console.error('Error approving PO:', error); } }; const handleReject = async (poId: string, reason: string) => { try { await rejectPO.mutateAsync({ tenantId, poId, reason }); refetchDashboard(); } catch (error) { console.error('Error rejecting PO:', error); } }; const handleViewDetails = (poId: string) => { // Open modal to show PO details in view mode setSelectedPOId(poId); setPOModalMode('view'); setIsPOModalOpen(true); }; const handleStartBatch = async (batchId: string) => { try { await startBatch.mutateAsync({ tenantId, batchId }); refetchDashboard(); } catch (error) { console.error('Error starting batch:', error); } }; // Calculate configuration sections for setup flow const setupSections = useMemo(() => { // Create safe fallbacks for icons to prevent React error #310 const SafePackageIcon = Package; const SafeUsersIcon = Users; const SafeBookOpenIcon = BookOpen; const SafeShieldIcon = Shield; // Validate that all icons are properly imported before using them const sections = [ { id: 'inventory', title: t('dashboard:config.inventory', 'Inventory'), icon: SafePackageIcon, path: '/app/database/inventory', count: ingredients.length, minimum: 3, recommended: 10, isComplete: ingredients.length >= 3, description: t('dashboard:config.add_ingredients', 'Add at least {{count}} ingredients', { count: 3 }), }, { id: 'suppliers', title: t('dashboard:config.suppliers', 'Suppliers'), icon: SafeUsersIcon, path: '/app/database/suppliers', count: suppliers.length, minimum: 1, recommended: 3, isComplete: suppliers.length >= 1, description: t('dashboard:config.add_supplier', 'Add your first supplier'), }, { id: 'recipes', title: t('dashboard:config.recipes', 'Recipes'), icon: SafeBookOpenIcon, path: '/app/database/recipes', count: recipes.length, minimum: 1, recommended: 3, isComplete: recipes.length >= 1, description: t('dashboard:config.add_recipe', 'Create your first recipe'), }, { id: 'quality', title: t('dashboard:config.quality', 'Quality Standards'), icon: SafeShieldIcon, path: '/app/operations/production/quality', count: qualityTemplates.length, minimum: 0, recommended: 2, isComplete: true, // Optional description: t('dashboard:config.add_quality', 'Add quality checks (optional)'), }, ]; return sections; }, [ingredients.length, suppliers.length, recipes.length, qualityTemplates.length, t]); // Calculate overall progress const { completedSections, totalSections, progressPercentage, criticalMissing, recommendedMissing } = useMemo(() => { // If data is still loading, use stored value as fallback to prevent flickering if (loadingIngredients || loadingSuppliers || loadingRecipes || loadingQuality) { return { completedSections: 0, totalSections: 4, // 4 required sections progressPercentage: setupProgressFromStorage, // Use stored value during loading criticalMissing: [], recommendedMissing: [], }; } // Guard against undefined or invalid setupSections if (!setupSections || !Array.isArray(setupSections) || setupSections.length === 0) { return { completedSections: 0, totalSections: 0, progressPercentage: 100, // Default to 100% to avoid blocking dashboard criticalMissing: [], recommendedMissing: [], }; } const requiredSections = setupSections.filter(s => s.id !== 'quality'); const completed = requiredSections.filter(s => s.isComplete).length; const total = requiredSections.length; const percentage = total > 0 ? Math.round((completed / total) * 100) : 100; const critical = setupSections.filter(s => !s.isComplete && s.id !== 'quality'); const recommended = setupSections.filter(s => s.count < s.recommended); // PHASE 1 OPTIMIZATION: Cache progress to localStorage for next page load try { localStorage.setItem(`setup_progress_${tenantId}`, percentage.toString()); } catch { // Ignore storage errors } return { completedSections: completed, totalSections: total, progressPercentage: percentage, criticalMissing: critical, recommendedMissing: recommended, }; }, [setupSections, tenantId, loadingIngredients, loadingSuppliers, loadingRecipes, loadingQuality, setupProgressFromStorage]); const handleAddWizardComplete = (itemType: ItemType, data?: any) => { console.log('Item created:', itemType, data); // SSE events will handle most updates automatically, but we refetch here // to ensure immediate feedback after user actions refetchDashboard(); }; // Keyboard shortcut for Quick Add (Cmd/Ctrl + K) useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { // Check for Cmd+K (Mac) or Ctrl+K (Windows/Linux) if ((event.metaKey || event.ctrlKey) && event.key === 'k') { event.preventDefault(); setIsAddWizardOpen(true); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, []); // Demo tour auto-start logic - using ref to prevent infinite loops useEffect(() => { // Only check tour start on initial render to prevent infinite loops if (typeof window !== 'undefined') { // Ensure we don't run this effect multiple times by checking a flag const tourCheckDone = sessionStorage.getItem('dashboard_tour_check_done'); if (!tourCheckDone) { sessionStorage.setItem('dashboard_tour_check_done', 'true'); console.log('[Dashboard] Demo mode:', isDemoMode); const shouldStart = shouldStartTour(); console.log('[Dashboard] Should start tour:', shouldStart); console.log('[Dashboard] SessionStorage demo_tour_should_start:', sessionStorage.getItem('demo_tour_should_start')); console.log('[Dashboard] SessionStorage demo_tour_start_step:', sessionStorage.getItem('demo_tour_start_step')); // Check if there's a tour intent from redirection (higher priority) const shouldStartFromRedirect = sessionStorage.getItem('demo_tour_should_start') === 'true'; const redirectStartStep = parseInt(sessionStorage.getItem('demo_tour_start_step') || '0', 10); if (isDemoMode && (shouldStart || shouldStartFromRedirect)) { console.log('[Dashboard] Starting tour in 1.5s...'); const timer = setTimeout(() => { console.log('[Dashboard] Executing startTour()'); if (shouldStartFromRedirect) { // Start tour from the specific step that was intended startTour(redirectStartStep); // Clear the redirect intent sessionStorage.removeItem('demo_tour_should_start'); sessionStorage.removeItem('demo_tour_start_step'); } else { // Start tour normally (from beginning or resume) startTour(); clearTourStartPending(); } }, 1500); return () => clearTimeout(timer); } } } }, [isDemoMode]); // Run only once after initial render // Note: startTour removed from deps to prevent infinite loop - the effect guards with sessionStorage ensure it only runs once return (
{/* Mobile-optimized container */}
{/* Header */}

{t('dashboard:title')}

{t('dashboard:subtitle')}

{/* Action Buttons */}
{/* Unified Add Button with Keyboard Shortcut */}
{/* Setup Flow - Three States */} {loadingIngredients || loadingSuppliers || loadingRecipes || loadingQuality ? ( /* Loading state - only show spinner until setup data is ready */
) : progressPercentage < 50 ? ( /* STATE 1: Critical Missing (<50%) - Full-page blocker */ ) : ( /* STATE 2 & 3: Dashboard with optional banner */ <> {/* Optional Setup Banner (50-99%) - Collapsible, Dismissible */} {progressPercentage < 100 && recommendedMissing.length > 0 && ( )} {/* Main Dashboard Layout - 4 New Focused Blocks */}
{/* BLOCK 1: System Status + AI Summary */}
{/* BLOCK 2: Pending Purchases (PO Approvals) */}
{/* BLOCK 3: Pending Deliveries (Overdue + Today) */}
{/* BLOCK 4: Production Status (Late/Running/Pending) */}
)}
{/* Mobile-friendly bottom padding */}
{/* Unified Add Wizard */} setIsAddWizardOpen(false)} onComplete={handleAddWizardComplete} /> {/* Purchase Order Details Modal - Using Unified Component */} {selectedPOId && ( { setIsPOModalOpen(false); setSelectedPOId(null); setPOModalMode('view'); // SSE events will handle most updates automatically refetchDashboard(); }} onApprove={handleApprove} onReject={handleReject} showApprovalActions={true} /> )}
); } /** * Main Dashboard Page * Conditionally renders either the Enterprise Dashboard or the Bakery Dashboard * based on the user's subscription tier. */ export function DashboardPage() { const { subscriptionInfo } = useSubscription(); const { currentTenant } = useTenant(); const { plan, loading } = subscriptionInfo; const tenantId = currentTenant?.id; if (loading) { return (
); } if (plan === SUBSCRIPTION_TIERS.ENTERPRISE) { return ; } return ; } export default DashboardPage;