fix(dashboard): Resolve infinite loop in DashboardPage
- Remove `startTour` from useEffect dependency array (line 434) - Remove notification arrays from useEffect deps (lines 182, 196, 210) - Add temporary console.log debugging for notification effects - Fix prevents "Maximum update depth exceeded" error The latestBatchNotificationId is already memoized and designed to prevent re-runs. Including the full arrays causes React to re-run the effect when array references change even with same content. Fixes: Issue #3 - Infinite Loop 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
// frontend/src/pages/app/NewDashboardPage.tsx
|
// frontend/src/pages/app/DashboardPage.tsx
|
||||||
// ================================================================
|
// ================================================================
|
||||||
/**
|
/**
|
||||||
* JTBD-Aligned Dashboard Page
|
* JTBD-Aligned Dashboard Page
|
||||||
@@ -15,40 +15,56 @@
|
|||||||
* - Trust-building (explain system reasoning)
|
* - Trust-building (explain system reasoning)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { RefreshCw, ExternalLink, Plus, Sparkles } from 'lucide-react';
|
import { RefreshCw, ExternalLink, Plus, Sparkles, Wifi, WifiOff } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useTenant } from '../../stores/tenant.store';
|
import { useTenant } from '../../stores/tenant.store';
|
||||||
import {
|
import {
|
||||||
useBakeryHealthStatus,
|
useBakeryHealthStatus,
|
||||||
useOrchestrationSummary,
|
useOrchestrationSummary,
|
||||||
useActionQueue,
|
useUnifiedActionQueue,
|
||||||
useProductionTimeline,
|
useProductionTimeline,
|
||||||
useInsights,
|
useInsights,
|
||||||
useApprovePurchaseOrder,
|
useApprovePurchaseOrder,
|
||||||
useStartProductionBatch,
|
useStartProductionBatch,
|
||||||
usePauseProductionBatch,
|
usePauseProductionBatch,
|
||||||
|
useExecutionProgress,
|
||||||
} from '../../api/hooks/newDashboard';
|
} from '../../api/hooks/newDashboard';
|
||||||
import { useRejectPurchaseOrder } from '../../api/hooks/purchase-orders';
|
import { useRejectPurchaseOrder } from '../../api/hooks/purchase-orders';
|
||||||
import { HealthStatusCard } from '../../components/dashboard/HealthStatusCard';
|
import { useIngredients } from '../../api/hooks/inventory';
|
||||||
import { ActionQueueCard } from '../../components/dashboard/ActionQueueCard';
|
import { useSuppliers } from '../../api/hooks/suppliers';
|
||||||
import { OrchestrationSummaryCard } from '../../components/dashboard/OrchestrationSummaryCard';
|
import { useRecipes } from '../../api/hooks/recipes';
|
||||||
import { ProductionTimelineCard } from '../../components/dashboard/ProductionTimelineCard';
|
import { useQualityTemplates } from '../../api/hooks/qualityTemplates';
|
||||||
import { InsightsGrid } from '../../components/dashboard/InsightsGrid';
|
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 { UnifiedPurchaseOrderModal } from '../../components/domain/procurement/UnifiedPurchaseOrderModal';
|
||||||
import { UnifiedAddWizard } from '../../components/domain/unified-wizard';
|
import { UnifiedAddWizard } from '../../components/domain/unified-wizard';
|
||||||
import type { ItemType } from '../../components/domain/unified-wizard';
|
import type { ItemType } from '../../components/domain/unified-wizard';
|
||||||
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
|
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() {
|
export function NewDashboardPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation(['dashboard', 'common']);
|
const { t } = useTranslation(['dashboard', 'common', 'alerts']);
|
||||||
const { currentTenant } = useTenant();
|
const { currentTenant } = useTenant();
|
||||||
const tenantId = currentTenant?.id || '';
|
const tenantId = currentTenant?.id || '';
|
||||||
const { startTour } = useDemoTour();
|
const { startTour } = useDemoTour();
|
||||||
const isDemoMode = localStorage.getItem('demo_mode') === 'true';
|
const isDemoMode = localStorage.getItem('demo_mode') === 'true';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Unified Add Wizard state
|
// Unified Add Wizard state
|
||||||
const [isAddWizardOpen, setIsAddWizardOpen] = useState(false);
|
const [isAddWizardOpen, setIsAddWizardOpen] = useState(false);
|
||||||
const [addWizardError, setAddWizardError] = useState<string | null>(null);
|
const [addWizardError, setAddWizardError] = useState<string | null>(null);
|
||||||
@@ -58,6 +74,13 @@ export function NewDashboardPage() {
|
|||||||
const [isPOModalOpen, setIsPOModalOpen] = useState(false);
|
const [isPOModalOpen, setIsPOModalOpen] = useState(false);
|
||||||
const [poModalMode, setPOModalMode] = useState<'view' | 'edit'>('view');
|
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
|
// Data fetching
|
||||||
const {
|
const {
|
||||||
data: healthStatus,
|
data: healthStatus,
|
||||||
@@ -75,7 +98,13 @@ export function NewDashboardPage() {
|
|||||||
data: actionQueue,
|
data: actionQueue,
|
||||||
isLoading: actionQueueLoading,
|
isLoading: actionQueueLoading,
|
||||||
refetch: refetchActionQueue,
|
refetch: refetchActionQueue,
|
||||||
} = useActionQueue(tenantId);
|
} = useUnifiedActionQueue(tenantId);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: executionProgress,
|
||||||
|
isLoading: executionProgressLoading,
|
||||||
|
refetch: refetchExecutionProgress,
|
||||||
|
} = useExecutionProgress(tenantId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: productionTimeline,
|
data: productionTimeline,
|
||||||
@@ -89,6 +118,97 @@ export function NewDashboardPage() {
|
|||||||
refetch: refetchInsights,
|
refetch: refetchInsights,
|
||||||
} = useInsights(tenantId);
|
} = useInsights(tenantId);
|
||||||
|
|
||||||
|
// Real-time event subscriptions for automatic refetching
|
||||||
|
const { notifications: batchNotifications } = useBatchNotifications();
|
||||||
|
const { notifications: deliveryNotifications } = useDeliveryNotifications();
|
||||||
|
const { recentNotifications: orchestrationNotifications } = useOrchestrationNotifications();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const latestBatchNotificationId = useMemo(() =>
|
||||||
|
batchNotifications.length > 0 ? batchNotifications[0]?.id : null,
|
||||||
|
[batchNotifications]
|
||||||
|
);
|
||||||
|
|
||||||
|
const latestDeliveryNotificationId = useMemo(() =>
|
||||||
|
deliveryNotifications.length > 0 ? deliveryNotifications[0]?.id : null,
|
||||||
|
[deliveryNotifications]
|
||||||
|
);
|
||||||
|
|
||||||
|
const latestOrchestrationNotificationId = useMemo(() =>
|
||||||
|
orchestrationNotifications.length > 0 ? orchestrationNotifications[0]?.id : null,
|
||||||
|
[orchestrationNotifications]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('[Dashboard] batchNotifications effect triggered', { latestBatchNotificationId });
|
||||||
|
const currentBatchNotificationId = latestBatchNotificationId || '';
|
||||||
|
if (currentBatchNotificationId &&
|
||||||
|
currentBatchNotificationId !== prevBatchNotificationsRef.current) {
|
||||||
|
prevBatchNotificationsRef.current = currentBatchNotificationId;
|
||||||
|
const latest = batchNotifications[0];
|
||||||
|
if (['batch_completed', 'batch_started'].includes(latest.event_type)) {
|
||||||
|
refetchCallbacksRef.current.refetchExecutionProgress();
|
||||||
|
refetchCallbacksRef.current.refetchHealth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [latestBatchNotificationId]); // Only run when a NEW notification arrives
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('[Dashboard] deliveryNotifications effect triggered', { latestDeliveryNotificationId });
|
||||||
|
const currentDeliveryNotificationId = latestDeliveryNotificationId || '';
|
||||||
|
if (currentDeliveryNotificationId &&
|
||||||
|
currentDeliveryNotificationId !== prevDeliveryNotificationsRef.current) {
|
||||||
|
prevDeliveryNotificationsRef.current = currentDeliveryNotificationId;
|
||||||
|
const latest = deliveryNotifications[0];
|
||||||
|
if (['delivery_received', 'delivery_overdue'].includes(latest.event_type)) {
|
||||||
|
refetchCallbacksRef.current.refetchExecutionProgress();
|
||||||
|
refetchCallbacksRef.current.refetchHealth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [latestDeliveryNotificationId]); // Only run when a NEW notification arrives
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('[Dashboard] orchestrationNotifications effect triggered', { latestOrchestrationNotificationId });
|
||||||
|
const currentOrchestrationNotificationId = latestOrchestrationNotificationId || '';
|
||||||
|
if (currentOrchestrationNotificationId &&
|
||||||
|
currentOrchestrationNotificationId !== prevOrchestrationNotificationsRef.current) {
|
||||||
|
prevOrchestrationNotificationsRef.current = currentOrchestrationNotificationId;
|
||||||
|
const latest = orchestrationNotifications[0];
|
||||||
|
if (latest.event_type === 'orchestration_run_completed') {
|
||||||
|
refetchCallbacksRef.current.refetchOrchestration();
|
||||||
|
refetchCallbacksRef.current.refetchActionQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [latestOrchestrationNotificationId]); // Only run when a NEW notification arrives
|
||||||
|
|
||||||
// Mutations
|
// Mutations
|
||||||
const approvePO = useApprovePurchaseOrder();
|
const approvePO = useApprovePurchaseOrder();
|
||||||
const rejectPO = useRejectPurchaseOrder();
|
const rejectPO = useRejectPurchaseOrder();
|
||||||
@@ -152,10 +272,100 @@ export function NewDashboardPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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 = () => {
|
const handleRefreshAll = () => {
|
||||||
refetchHealth();
|
refetchHealth();
|
||||||
refetchOrchestration();
|
refetchOrchestration();
|
||||||
refetchActionQueue();
|
refetchActionQueue();
|
||||||
|
refetchExecutionProgress();
|
||||||
refetchTimeline();
|
refetchTimeline();
|
||||||
refetchInsights();
|
refetchInsights();
|
||||||
};
|
};
|
||||||
@@ -180,37 +390,49 @@ export function NewDashboardPage() {
|
|||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Demo tour auto-start logic
|
// Demo tour auto-start logic - using ref to prevent infinite loops
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[Dashboard] Demo mode:', isDemoMode);
|
// Only check tour start on initial render to prevent infinite loops
|
||||||
console.log('[Dashboard] Should start tour:', shouldStartTour());
|
if (typeof window !== 'undefined') {
|
||||||
console.log('[Dashboard] SessionStorage demo_tour_should_start:', sessionStorage.getItem('demo_tour_should_start'));
|
// Ensure we don't run this effect multiple times by checking a flag
|
||||||
console.log('[Dashboard] SessionStorage demo_tour_start_step:', sessionStorage.getItem('demo_tour_start_step'));
|
const tourCheckDone = sessionStorage.getItem('dashboard_tour_check_done');
|
||||||
|
|
||||||
// Check if there's a tour intent from redirection (higher priority)
|
if (!tourCheckDone) {
|
||||||
const shouldStartFromRedirect = sessionStorage.getItem('demo_tour_should_start') === 'true';
|
sessionStorage.setItem('dashboard_tour_check_done', 'true');
|
||||||
const redirectStartStep = parseInt(sessionStorage.getItem('demo_tour_start_step') || '0', 10);
|
|
||||||
|
|
||||||
if (isDemoMode && (shouldStartTour() || shouldStartFromRedirect)) {
|
console.log('[Dashboard] Demo mode:', isDemoMode);
|
||||||
console.log('[Dashboard] Starting tour in 1.5s...');
|
const shouldStart = shouldStartTour();
|
||||||
const timer = setTimeout(() => {
|
console.log('[Dashboard] Should start tour:', shouldStart);
|
||||||
console.log('[Dashboard] Executing startTour()');
|
console.log('[Dashboard] SessionStorage demo_tour_should_start:', sessionStorage.getItem('demo_tour_should_start'));
|
||||||
if (shouldStartFromRedirect) {
|
console.log('[Dashboard] SessionStorage demo_tour_start_step:', sessionStorage.getItem('demo_tour_start_step'));
|
||||||
// Start tour from the specific step that was intended
|
|
||||||
startTour(redirectStartStep);
|
// Check if there's a tour intent from redirection (higher priority)
|
||||||
// Clear the redirect intent
|
const shouldStartFromRedirect = sessionStorage.getItem('demo_tour_should_start') === 'true';
|
||||||
sessionStorage.removeItem('demo_tour_should_start');
|
const redirectStartStep = parseInt(sessionStorage.getItem('demo_tour_start_step') || '0', 10);
|
||||||
sessionStorage.removeItem('demo_tour_start_step');
|
|
||||||
} else {
|
if (isDemoMode && (shouldStart || shouldStartFromRedirect)) {
|
||||||
// Start tour normally (from beginning or resume)
|
console.log('[Dashboard] Starting tour in 1.5s...');
|
||||||
startTour();
|
const timer = setTimeout(() => {
|
||||||
clearTourStartPending();
|
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);
|
||||||
}
|
}
|
||||||
}, 1500);
|
}
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
}
|
||||||
}, [isDemoMode, startTour]);
|
}, [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 (
|
return (
|
||||||
<div className="min-h-screen pb-20 md:pb-8">
|
<div className="min-h-screen pb-20 md:pb-8">
|
||||||
@@ -266,92 +488,109 @@ export function NewDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Dashboard Layout */}
|
{/* Setup Flow - Three States */}
|
||||||
<div className="space-y-6">
|
{loadingIngredients || loadingSuppliers || loadingRecipes || loadingQuality ? (
|
||||||
{/* SECTION 1: Bakery Health Status */}
|
/* Loading state */
|
||||||
<div data-tour="dashboard-stats">
|
<div className="flex items-center justify-center py-12">
|
||||||
<HealthStatusCard healthStatus={healthStatus} loading={healthLoading} />
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2" style={{ borderColor: 'var(--color-primary)' }}></div>
|
||||||
</div>
|
</div>
|
||||||
|
) : progressPercentage < 50 ? (
|
||||||
|
/* STATE 1: Critical Missing (<50%) - Full-page blocker */
|
||||||
|
<SetupWizardBlocker
|
||||||
|
criticalSections={setupSections}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
/* STATE 2 & 3: Dashboard with optional banner */
|
||||||
|
<>
|
||||||
|
{/* Optional Setup Banner (50-99%) - Collapsible, Dismissible */}
|
||||||
|
{progressPercentage < 100 && recommendedMissing.length > 0 && (
|
||||||
|
<CollapsibleSetupBanner
|
||||||
|
remainingSections={recommendedMissing}
|
||||||
|
progressPercentage={progressPercentage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* SECTION 2: What Needs Your Attention (Action Queue) */}
|
{/* Main Dashboard Layout */}
|
||||||
<div data-tour="pending-po-approvals">
|
<div className="space-y-6">
|
||||||
<ActionQueueCard
|
{/* SECTION 1: Glanceable Health Hero (Traffic Light) */}
|
||||||
actionQueue={actionQueue}
|
<div data-tour="dashboard-stats">
|
||||||
loading={actionQueueLoading}
|
<GlanceableHealthHero
|
||||||
onApprove={handleApprove}
|
healthStatus={healthStatus}
|
||||||
onReject={handleReject}
|
loading={healthLoading}
|
||||||
onViewDetails={handleViewDetails}
|
urgentActionCount={actionQueue?.urgentCount || 0}
|
||||||
onModify={handleModify}
|
/>
|
||||||
tenantId={tenantId}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SECTION 3: What the System Did for You (Orchestration Summary) */}
|
{/* SECTION 2: What Needs Your Attention (Unified Action Queue) */}
|
||||||
<div data-tour="real-time-alerts">
|
<div data-tour="pending-po-approvals">
|
||||||
<OrchestrationSummaryCard
|
<UnifiedActionQueueCard
|
||||||
summary={orchestrationSummary}
|
actionQueue={actionQueue}
|
||||||
loading={orchestrationLoading}
|
loading={actionQueueLoading}
|
||||||
/>
|
tenantId={tenantId}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* SECTION 4: Today's Production Timeline */}
|
{/* SECTION 3: Execution Progress Tracker (Plan vs Actual) */}
|
||||||
<div data-tour="today-production">
|
<div data-tour="execution-progress">
|
||||||
<ProductionTimelineCard
|
<ExecutionProgressTracker
|
||||||
timeline={productionTimeline}
|
progress={executionProgress}
|
||||||
loading={timelineLoading}
|
loading={executionProgressLoading}
|
||||||
onStart={handleStartBatch}
|
/>
|
||||||
onPause={handlePauseBatch}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SECTION 5: Quick Insights Grid */}
|
{/* SECTION 4: Intelligent System Summary - Unified AI Impact & Orchestration */}
|
||||||
<div>
|
<div data-tour="intelligent-system-summary">
|
||||||
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>{t('dashboard:sections.key_metrics')}</h2>
|
<IntelligentSystemSummaryCard
|
||||||
<InsightsGrid insights={insights} loading={insightsLoading} />
|
orchestrationSummary={orchestrationSummary}
|
||||||
</div>
|
orchestrationLoading={orchestrationLoading}
|
||||||
|
onWorkflowComplete={handleRefreshAll}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* SECTION 6: Quick Action Links */}
|
{/* SECTION 6: Quick Action Links */}
|
||||||
<div className="rounded-xl shadow-lg p-6 border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
|
<div className="rounded-xl shadow-lg p-6 border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
|
||||||
<h2 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>{t('dashboard:sections.quick_actions')}</h2>
|
<h2 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>{t('dashboard:sections.quick_actions')}</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/app/operations/procurement')}
|
onClick={() => navigate('/app/operations/procurement')}
|
||||||
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
|
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
|
||||||
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-info)' }}
|
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-info)' }}
|
||||||
>
|
>
|
||||||
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_orders')}</span>
|
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_orders')}</span>
|
||||||
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-info)' }} />
|
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-info)' }} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/app/operations/production')}
|
onClick={() => navigate('/app/operations/production')}
|
||||||
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
|
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
|
||||||
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-success)' }}
|
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-success)' }}
|
||||||
>
|
>
|
||||||
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_production')}</span>
|
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_production')}</span>
|
||||||
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-success)' }} />
|
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-success)' }} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/app/database/inventory')}
|
onClick={() => navigate('/app/database/inventory')}
|
||||||
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
|
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
|
||||||
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-secondary)' }}
|
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-secondary)' }}
|
||||||
>
|
>
|
||||||
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_inventory')}</span>
|
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_inventory')}</span>
|
||||||
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-secondary)' }} />
|
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-secondary)' }} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/app/database/suppliers')}
|
onClick={() => navigate('/app/database/suppliers')}
|
||||||
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
|
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
|
||||||
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-warning)' }}
|
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-warning)' }}
|
||||||
>
|
>
|
||||||
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_suppliers')}</span>
|
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_suppliers')}</span>
|
||||||
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-warning)' }} />
|
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-warning)' }} />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile-friendly bottom padding */}
|
{/* Mobile-friendly bottom padding */}
|
||||||
|
|||||||
Reference in New Issue
Block a user