Add frontend loading imporvements

This commit is contained in:
Urtzi Alfaro
2025-12-27 21:30:42 +01:00
parent 6e3a6590d6
commit 54662dde79
21 changed files with 799 additions and 363 deletions

View File

@@ -25,15 +25,13 @@ import {
useApprovePurchaseOrder,
useStartProductionBatch,
} from '../../api/hooks/useProfessionalDashboard';
import { useControlPanelData, useControlPanelRealtimeSync } from '../../api/hooks/useControlPanelData';
import { useControlPanelData } from '../../api/hooks/useControlPanelData';
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 { useUserProgress } from '../../api/hooks/onboarding';
import { useQualityTemplates } from '../../api/hooks/qualityTemplates';
import { useOnboardingStatus } from '../../hooks/useOnboardingStatus';
import { SetupWizardBlocker } from '../../components/dashboard/SetupWizardBlocker';
import { CollapsibleSetupBanner } from '../../components/dashboard/CollapsibleSetupBanner';
import { DashboardSkeleton } from '../../components/dashboard/DashboardSkeleton';
import {
SystemStatusBlock,
PendingPurchasesBlock,
@@ -69,49 +67,27 @@ export function BakeryDashboard({ plan }: { plan?: string }) {
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]);
// ALWAYS use lightweight onboarding status endpoint for ALL users (demo + authenticated)
// This is faster and more efficient than fetching full datasets
const { data: onboardingStatus, isLoading: loadingOnboarding } = useOnboardingStatus(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 : [];
// DEBUG: Log onboarding status
useEffect(() => {
console.log('[DashboardPage] Onboarding Status:', {
onboardingStatus,
loadingOnboarding,
tenantId,
});
}, [onboardingStatus, loadingOnboarding, tenantId]);
// NEW: Enhanced control panel data fetch with SSE integration
// Note: useControlPanelData already includes SSE integration and auto-refetch
const {
data: dashboardData,
isLoading: dashboardLoading,
refetch: refetchDashboard,
} = useControlPanelData(tenantId);
// Enable enhanced SSE real-time state synchronization
useControlPanelRealtimeSync(tenantId);
// Mutations
const approvePO = useApprovePurchaseOrder();
const rejectPO = useRejectPurchaseOrder();
@@ -161,6 +137,12 @@ export function BakeryDashboard({ plan }: { plan?: string }) {
const SafeBookOpenIcon = BookOpen;
const SafeShieldIcon = Shield;
// ALWAYS use onboardingStatus counts for ALL users
// This is lightweight and doesn't require fetching full datasets
const ingredientsCount = onboardingStatus?.ingredients_count ?? 0;
const suppliersCount = onboardingStatus?.suppliers_count ?? 0;
const recipesCount = onboardingStatus?.recipes_count ?? 0;
// Validate that all icons are properly imported before using them
const sections = [
{
@@ -168,10 +150,10 @@ export function BakeryDashboard({ plan }: { plan?: string }) {
title: t('dashboard:config.inventory', 'Inventory'),
icon: SafePackageIcon,
path: '/app/database/inventory',
count: ingredients.length,
count: ingredientsCount,
minimum: 3,
recommended: 10,
isComplete: ingredients.length >= 3,
isComplete: ingredientsCount >= 3,
description: t('dashboard:config.add_ingredients', 'Add at least {{count}} ingredients', { count: 3 }),
},
{
@@ -179,10 +161,10 @@ export function BakeryDashboard({ plan }: { plan?: string }) {
title: t('dashboard:config.suppliers', 'Suppliers'),
icon: SafeUsersIcon,
path: '/app/database/suppliers',
count: suppliers.length,
count: suppliersCount,
minimum: 1,
recommended: 3,
isComplete: suppliers.length >= 1,
isComplete: suppliersCount >= 1,
description: t('dashboard:config.add_supplier', 'Add your first supplier'),
},
{
@@ -190,10 +172,10 @@ export function BakeryDashboard({ plan }: { plan?: string }) {
title: t('dashboard:config.recipes', 'Recipes'),
icon: SafeBookOpenIcon,
path: '/app/database/recipes',
count: recipes.length,
count: recipesCount,
minimum: 1,
recommended: 3,
isComplete: recipes.length >= 1,
isComplete: recipesCount >= 1,
description: t('dashboard:config.add_recipe', 'Create your first recipe'),
},
{
@@ -201,30 +183,50 @@ export function BakeryDashboard({ plan }: { plan?: string }) {
title: t('dashboard:config.quality', 'Quality Standards'),
icon: SafeShieldIcon,
path: '/app/operations/production/quality',
count: qualityTemplates.length,
count: 0, // Quality templates are optional, not tracked in onboarding
minimum: 0,
recommended: 2,
isComplete: true, // Optional
isComplete: true, // Optional - always complete
description: t('dashboard:config.add_quality', 'Add quality checks (optional)'),
},
];
return sections;
}, [ingredients.length, suppliers.length, recipes.length, qualityTemplates.length, t]);
}, [onboardingStatus, 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) {
// If onboarding data is still loading, show loading state
if (loadingOnboarding) {
console.log('[DashboardPage] Progress calculation: Loading state');
return {
completedSections: 0,
totalSections: 4, // 4 required sections
progressPercentage: setupProgressFromStorage, // Use stored value during loading
progressPercentage: 0, // Loading state
criticalMissing: [],
recommendedMissing: [],
};
}
// OPTIMIZATION: If we have onboarding status from API, use it directly
if (onboardingStatus?.progress_percentage !== undefined) {
const apiProgress = onboardingStatus.progress_percentage;
console.log('[DashboardPage] Progress calculation: Using API progress', {
apiProgress,
has_minimum_setup: onboardingStatus.has_minimum_setup,
onboardingStatus,
});
return {
completedSections: onboardingStatus.has_minimum_setup ? 3 : 0,
totalSections: 3,
progressPercentage: apiProgress,
criticalMissing: apiProgress < 50 ? setupSections.filter(s => s.id !== 'quality' && !s.isComplete) : [],
recommendedMissing: setupSections.filter(s => s.count < s.recommended),
};
}
console.log('[DashboardPage] Progress calculation: Fallback to manual calculation');
// Guard against undefined or invalid setupSections
if (!setupSections || !Array.isArray(setupSections) || setupSections.length === 0) {
return {
@@ -258,7 +260,7 @@ export function BakeryDashboard({ plan }: { plan?: string }) {
criticalMissing: critical,
recommendedMissing: recommended,
};
}, [setupSections, tenantId, loadingIngredients, loadingSuppliers, loadingRecipes, loadingQuality, setupProgressFromStorage]);
}, [onboardingStatus, setupSections, tenantId, loadingOnboarding]);
const handleAddWizardComplete = (itemType: ItemType, data?: any) => {
console.log('Item created:', itemType, data);
@@ -302,7 +304,7 @@ export function BakeryDashboard({ plan }: { plan?: string }) {
const redirectStartStep = parseInt(sessionStorage.getItem('demo_tour_start_step') || '0', 10);
if (isDemoMode && (shouldStart || shouldStartFromRedirect)) {
console.log('[Dashboard] Starting tour in 1.5s...');
console.log('[Dashboard] Starting tour...');
const timer = setTimeout(() => {
console.log('[Dashboard] Executing startTour()');
if (shouldStartFromRedirect) {
@@ -316,7 +318,7 @@ export function BakeryDashboard({ plan }: { plan?: string }) {
startTour();
clearTourStartPending();
}
}, 1500);
}, 300);
return () => clearTimeout(timer);
}
@@ -362,8 +364,8 @@ export function BakeryDashboard({ plan }: { plan?: string }) {
</div>
{/* Setup Flow - Three States */}
{loadingIngredients || loadingSuppliers || loadingRecipes || loadingQuality ? (
/* Loading state - only show spinner until setup data is ready */
{loadingOnboarding ? (
/* Loading state for onboarding checks */
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2" style={{ borderColor: 'var(--color-primary)' }}></div>
</div>
@@ -384,62 +386,66 @@ export function BakeryDashboard({ plan }: { plan?: string }) {
)}
{/* Main Dashboard Layout - 4 New Focused Blocks */}
<div className="space-y-6">
{/* BLOCK 1: System Status + AI Summary */}
<div data-tour="dashboard-stats">
<SystemStatusBlock
data={dashboardData}
loading={dashboardLoading}
/>
</div>
{/* BLOCK 2: Pending Purchases (PO Approvals) */}
<div data-tour="pending-po-approvals">
<PendingPurchasesBlock
pendingPOs={dashboardData?.pendingPOs || []}
loading={dashboardLoading}
onApprove={handleApprove}
onReject={handleReject}
onViewDetails={handleViewDetails}
/>
</div>
{/* BLOCK 3: Pending Deliveries (Overdue + Today) */}
<div data-tour="pending-deliveries">
<PendingDeliveriesBlock
overdueDeliveries={dashboardData?.overdueDeliveries || []}
pendingDeliveries={dashboardData?.pendingDeliveries || []}
loading={dashboardLoading}
/>
</div>
{/* BLOCK 4: Production Status (Late/Running/Pending) */}
<div data-tour="execution-progress">
<ProductionStatusBlock
lateToStartBatches={dashboardData?.lateToStartBatches || []}
runningBatches={dashboardData?.runningBatches || []}
pendingBatches={dashboardData?.pendingBatches || []}
alerts={dashboardData?.alerts || []}
equipmentAlerts={dashboardData?.equipmentAlerts || []}
loading={dashboardLoading}
onStartBatch={handleStartBatch}
/>
</div>
{/* BLOCK 5: AI Insights (Professional/Enterprise only) */}
{(plan === SUBSCRIPTION_TIERS.PROFESSIONAL || plan === SUBSCRIPTION_TIERS.ENTERPRISE) && (
<div data-tour="ai-insights">
<AIInsightsBlock
insights={dashboardData?.aiInsights || []}
loading={dashboardLoading}
onViewAll={() => {
// Navigate to AI Insights page
window.location.href = '/app/analytics/ai-insights';
}}
{dashboardLoading ? (
<DashboardSkeleton />
) : (
<div className="space-y-6">
{/* BLOCK 1: System Status + AI Summary */}
<div data-tour="dashboard-stats">
<SystemStatusBlock
data={dashboardData}
loading={false}
/>
</div>
)}
</div>
{/* BLOCK 2: Pending Purchases (PO Approvals) */}
<div data-tour="pending-po-approvals">
<PendingPurchasesBlock
pendingPOs={dashboardData?.pendingPOs || []}
loading={false}
onApprove={handleApprove}
onReject={handleReject}
onViewDetails={handleViewDetails}
/>
</div>
{/* BLOCK 3: Pending Deliveries (Overdue + Today) */}
<div data-tour="pending-deliveries">
<PendingDeliveriesBlock
overdueDeliveries={dashboardData?.overdueDeliveries || []}
pendingDeliveries={dashboardData?.pendingDeliveries || []}
loading={false}
/>
</div>
{/* BLOCK 4: Production Status (Late/Running/Pending) */}
<div data-tour="execution-progress">
<ProductionStatusBlock
lateToStartBatches={dashboardData?.lateToStartBatches || []}
runningBatches={dashboardData?.runningBatches || []}
pendingBatches={dashboardData?.pendingBatches || []}
alerts={dashboardData?.alerts || []}
equipmentAlerts={dashboardData?.equipmentAlerts || []}
loading={false}
onStartBatch={handleStartBatch}
/>
</div>
{/* BLOCK 5: AI Insights (Professional/Enterprise only) */}
{(plan === SUBSCRIPTION_TIERS.PROFESSIONAL || plan === SUBSCRIPTION_TIERS.ENTERPRISE) && (
<div data-tour="ai-insights">
<AIInsightsBlock
insights={dashboardData?.aiInsights || []}
loading={false}
onViewAll={() => {
// Navigate to AI Insights page
window.location.href = '/app/analytics/ai-insights';
}}
/>
</div>
)}
</div>
)}
</>
)}
</div>

View File

@@ -295,10 +295,10 @@ const DemoPage = () => {
// BUG-010 FIX: Handle ready status separately from partial
if (statusData.status === 'ready') {
// Full success - set to 100% and navigate after delay
// Full success - set to 100% and navigate immediately
clearInterval(progressInterval);
setCloneProgress(prev => ({ ...prev, overall: 100 }));
setTimeout(() => {
requestAnimationFrame(() => {
// Reset state before navigation
setCreatingTier(null);
setProgressStartTime(null);
@@ -311,7 +311,7 @@ const DemoPage = () => {
});
// Navigate to the main dashboard which will automatically route to enterprise or bakery dashboard based on subscription tier
navigate('/app/dashboard');
}, 1500); // Increased from 1000ms to show 100% completion
});
return;
} else if (statusData.status === 'PARTIAL' || statusData.status === 'partial') {
// BUG-010 FIX: Show warning modal for partial status