// ================================================================ // 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 React, { useState, useEffect, useMemo, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { RefreshCw, ExternalLink, Plus, Sparkles, Wifi, WifiOff } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useTenant } from '../../stores/tenant.store'; import { useBakeryHealthStatus, useOrchestrationSummary, useUnifiedActionQueue, useProductionTimeline, useInsights, useApprovePurchaseOrder, useStartProductionBatch, usePauseProductionBatch, useExecutionProgress, } from '../../api/hooks/newDashboard'; 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 { GlanceableHealthHero } from '../../components/dashboard/GlanceableHealthHero'; import { SetupWizardBlocker } from '../../components/dashboard/SetupWizardBlocker'; import { CollapsibleSetupBanner } from '../../components/dashboard/CollapsibleSetupBanner'; import { UnifiedActionQueueCard } from '../../components/dashboard/UnifiedActionQueueCard'; import { ExecutionProgressTracker } from '../../components/dashboard/ExecutionProgressTracker'; import { IntelligentSystemSummaryCard } from '../../components/dashboard/IntelligentSystemSummaryCard'; import { useAuthUser } from '../../stores'; 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 { useBatchNotifications, useDeliveryNotifications, useOrchestrationNotifications, } from '../../hooks'; export function NewDashboardPage() { const navigate = useNavigate(); 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); const [addWizardError, setAddWizardError] = useState(null); // PO Details Modal state const [selectedPOId, setSelectedPOId] = useState(null); const [isPOModalOpen, setIsPOModalOpen] = useState(false); const [poModalMode, setPOModalMode] = useState<'view' | 'edit'>('view'); // Setup Progress Data 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 : []; // Data fetching const { data: healthStatus, isLoading: healthLoading, refetch: refetchHealth, } = useBakeryHealthStatus(tenantId); const { data: orchestrationSummary, isLoading: orchestrationLoading, refetch: refetchOrchestration, } = useOrchestrationSummary(tenantId); const { data: actionQueue, isLoading: actionQueueLoading, refetch: refetchActionQueue, } = useUnifiedActionQueue(tenantId); const { data: executionProgress, isLoading: executionProgressLoading, refetch: refetchExecutionProgress, } = useExecutionProgress(tenantId); const { data: productionTimeline, isLoading: timelineLoading, refetch: refetchTimeline, } = useProductionTimeline(tenantId); const { data: insights, isLoading: insightsLoading, refetch: refetchInsights, } = useInsights(tenantId); // Real-time event subscriptions for automatic refetching const { notifications: batchNotifications } = useBatchNotifications(); const { notifications: deliveryNotifications } = useDeliveryNotifications(); const { recentNotifications: orchestrationNotifications } = useOrchestrationNotifications(); console.log('🔄 [Dashboard] Component render - notification counts:', { batch: batchNotifications.length, delivery: deliveryNotifications.length, orchestration: orchestrationNotifications.length, batchIds: batchNotifications.map(n => n.id).join(','), deliveryIds: deliveryNotifications.map(n => n.id).join(','), orchestrationIds: orchestrationNotifications.map(n => n.id).join(','), }); // SSE connection status const sseConnected = true; // Simplified - based on other notification hooks // Store refetch callbacks in a ref to prevent infinite loop from dependency changes // React Query refetch functions are recreated on every query state change, which would // trigger useEffect again if they were in the dependency array const refetchCallbacksRef = useRef({ refetchActionQueue, refetchHealth, refetchExecutionProgress, refetchOrchestration, }); // Store previous notification IDs to prevent infinite refetch loops const prevBatchNotificationsRef = useRef(''); const prevDeliveryNotificationsRef = useRef(''); const prevOrchestrationNotificationsRef = useRef(''); // Update ref with latest callbacks on every render useEffect(() => { refetchCallbacksRef.current = { refetchActionQueue, refetchHealth, refetchExecutionProgress, refetchOrchestration, }; }); // Track the latest notification ID to prevent re-running on same notification // Use stringified ID array to create stable dependency that only changes when IDs actually change const batchIdsString = JSON.stringify(batchNotifications.map(n => n.id)); const deliveryIdsString = JSON.stringify(deliveryNotifications.map(n => n.id)); const orchestrationIdsString = JSON.stringify(orchestrationNotifications.map(n => n.id)); console.log('📝 [Dashboard] Stringified ID arrays:', { batchIdsString, deliveryIdsString, orchestrationIdsString, }); const latestBatchNotificationId = useMemo(() => { const result = batchNotifications.length === 0 ? '' : (batchNotifications[0]?.id || ''); console.log('🧮 [Dashboard] latestBatchNotificationId useMemo recalculated:', { result, dependency: batchIdsString, notificationCount: batchNotifications.length, }); return result; }, [batchIdsString]); const latestDeliveryNotificationId = useMemo(() => { const result = deliveryNotifications.length === 0 ? '' : (deliveryNotifications[0]?.id || ''); console.log('🧮 [Dashboard] latestDeliveryNotificationId useMemo recalculated:', { result, dependency: deliveryIdsString, notificationCount: deliveryNotifications.length, }); return result; }, [deliveryIdsString]); const latestOrchestrationNotificationId = useMemo(() => { const result = orchestrationNotifications.length === 0 ? '' : (orchestrationNotifications[0]?.id || ''); console.log('🧮 [Dashboard] latestOrchestrationNotificationId useMemo recalculated:', { result, dependency: orchestrationIdsString, notificationCount: orchestrationNotifications.length, }); return result; }, [orchestrationIdsString]); useEffect(() => { console.log('⚡ [Dashboard] batchNotifications useEffect triggered', { latestBatchNotificationId, prevValue: prevBatchNotificationsRef.current, hasChanged: latestBatchNotificationId !== prevBatchNotificationsRef.current, notificationCount: batchNotifications.length, firstNotification: batchNotifications[0], }); if (latestBatchNotificationId && latestBatchNotificationId !== prevBatchNotificationsRef.current) { console.log('🔥 [Dashboard] NEW batch notification detected, updating ref and refetching'); prevBatchNotificationsRef.current = latestBatchNotificationId; const latest = batchNotifications[0]; if (['batch_completed', 'batch_started'].includes(latest.event_type)) { console.log('🚀 [Dashboard] Triggering refetch for batch event:', latest.event_type); refetchCallbacksRef.current.refetchExecutionProgress(); refetchCallbacksRef.current.refetchHealth(); } else { console.log('⏭️ [Dashboard] Skipping refetch - event type not relevant:', latest.event_type); } } }, [latestBatchNotificationId]); // Only run when a NEW notification arrives useEffect(() => { console.log('⚡ [Dashboard] deliveryNotifications useEffect triggered', { latestDeliveryNotificationId, prevValue: prevDeliveryNotificationsRef.current, hasChanged: latestDeliveryNotificationId !== prevDeliveryNotificationsRef.current, notificationCount: deliveryNotifications.length, firstNotification: deliveryNotifications[0], }); if (latestDeliveryNotificationId && latestDeliveryNotificationId !== prevDeliveryNotificationsRef.current) { console.log('🔥 [Dashboard] NEW delivery notification detected, updating ref and refetching'); prevDeliveryNotificationsRef.current = latestDeliveryNotificationId; const latest = deliveryNotifications[0]; if (['delivery_received', 'delivery_overdue'].includes(latest.event_type)) { console.log('🚀 [Dashboard] Triggering refetch for delivery event:', latest.event_type); refetchCallbacksRef.current.refetchExecutionProgress(); refetchCallbacksRef.current.refetchHealth(); } else { console.log('⏭️ [Dashboard] Skipping refetch - event type not relevant:', latest.event_type); } } }, [latestDeliveryNotificationId]); // Only run when a NEW notification arrives useEffect(() => { console.log('⚡ [Dashboard] orchestrationNotifications useEffect triggered', { latestOrchestrationNotificationId, prevValue: prevOrchestrationNotificationsRef.current, hasChanged: latestOrchestrationNotificationId !== prevOrchestrationNotificationsRef.current, notificationCount: orchestrationNotifications.length, firstNotification: orchestrationNotifications[0], }); if (latestOrchestrationNotificationId && latestOrchestrationNotificationId !== prevOrchestrationNotificationsRef.current) { console.log('🔥 [Dashboard] NEW orchestration notification detected, updating ref and refetching'); prevOrchestrationNotificationsRef.current = latestOrchestrationNotificationId; const latest = orchestrationNotifications[0]; if (latest.event_type === 'orchestration_run_completed') { console.log('🚀 [Dashboard] Triggering refetch for orchestration event:', latest.event_type); refetchCallbacksRef.current.refetchOrchestration(); refetchCallbacksRef.current.refetchActionQueue(); } else { console.log('⏭️ [Dashboard] Skipping refetch - event type not relevant:', latest.event_type); } } }, [latestOrchestrationNotificationId]); // Only run when a NEW notification arrives // Mutations const approvePO = useApprovePurchaseOrder(); const rejectPO = useRejectPurchaseOrder(); const startBatch = useStartProductionBatch(); const pauseBatch = usePauseProductionBatch(); // Handlers const handleApprove = async (actionId: string) => { try { await approvePO.mutateAsync({ tenantId, poId: actionId }); // Refetch to update UI refetchActionQueue(); refetchHealth(); } catch (error) { console.error('Error approving PO:', error); } }; const handleReject = async (actionId: string, reason: string) => { try { await rejectPO.mutateAsync({ tenantId, poId: actionId, reason }); // Refetch to update UI refetchActionQueue(); refetchHealth(); } catch (error) { console.error('Error rejecting PO:', error); } }; const handleViewDetails = (actionId: string) => { // Open modal to show PO details in view mode setSelectedPOId(actionId); setPOModalMode('view'); setIsPOModalOpen(true); }; const handleModify = (actionId: string) => { // Open modal to edit PO details setSelectedPOId(actionId); setPOModalMode('edit'); setIsPOModalOpen(true); }; const handleStartBatch = async (batchId: string) => { try { await startBatch.mutateAsync({ tenantId, batchId }); refetchTimeline(); refetchHealth(); } catch (error) { console.error('Error starting batch:', error); } }; const handlePauseBatch = async (batchId: string) => { try { await pauseBatch.mutateAsync({ tenantId, batchId }); refetchTimeline(); refetchHealth(); } catch (error) { console.error('Error pausing 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(() => { // 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); return { completedSections: completed, totalSections: total, progressPercentage: percentage, criticalMissing: critical, recommendedMissing: recommended, }; }, [setupSections]); const handleRefreshAll = () => { refetchHealth(); refetchOrchestration(); refetchActionQueue(); refetchExecutionProgress(); refetchTimeline(); refetchInsights(); }; const handleAddWizardComplete = (itemType: ItemType, data?: any) => { console.log('Item created:', itemType, data); // Refetch relevant data based on what was added handleRefreshAll(); }; // 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 */
) : 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 */}
{/* SECTION 1: Glanceable Health Hero (Traffic Light) */}
{/* SECTION 2: What Needs Your Attention (Unified Action Queue) */}
{/* SECTION 3: Execution Progress Tracker (Plan vs Actual) */}
{/* SECTION 4: Intelligent System Summary - Unified AI Impact & Orchestration */}
{/* SECTION 6: Quick Action Links */}

{t('dashboard:sections.quick_actions')}

)}
{/* 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'); handleRefreshAll(); }} onApprove={handleApprove} onReject={handleReject} showApprovalActions={true} /> )}
); } export default NewDashboardPage;