import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button, Card, CardHeader, CardBody, CardFooter, CardTitle, CardDescription, Badge, Alert, AlertDescription, Modal, ModalHeader, ModalBody, ModalFooter } from '../../components/ui'; import { apiClient } from '../../api/client'; import { useAuthStore } from '../../stores'; import { PublicLayout } from '../../components/layout'; import { useTranslation } from 'react-i18next'; import { Store, Network, CheckCircle, Users, Building, Package, BarChart3, ChefHat, CreditCard, Bell, ShoppingBag, ShoppingBasket, Warehouse, Truck, TrendingUp, DollarSign, Clock, MapPin, Layers, Activity, ShoppingCart, Factory, Zap, Star, PlusCircle, Map as MapIcon, ShoppingBasket as ShoppingBasketIcon, TrendingUp as ChartIcon, DollarSign as MoneyIcon, ArrowRight, Sparkles } from 'lucide-react'; const DemoPage = () => { const navigate = useNavigate(); const { t } = useTranslation('demo'); const setDemoAuth = useAuthStore((state) => state.setDemoAuth); const [selectedTier, setSelectedTier] = useState(null); const [creatingTier, setCreatingTier] = useState(null); const [cloneProgress, setCloneProgress] = useState({ parent: 0, children: [0, 0, 0], distribution: 0, overall: 0 }); const [creationError, setCreationError] = useState(''); const [estimatedProgress, setEstimatedProgress] = useState(0); const [progressStartTime, setProgressStartTime] = useState(null); const [estimatedRemainingSeconds, setEstimatedRemainingSeconds] = useState(null); // BUG-010 FIX: State for partial status warning const [partialWarning, setPartialWarning] = useState<{ show: boolean; message: string; failedServices: string[]; sessionData: any; tier: string; } | null>(null); // BUG-011 FIX: State for timeout modal const [timeoutModal, setTimeoutModal] = useState<{ show: boolean; sessionId: string; sessionData: any; tier: string; } | null>(null); // BUG-009 FIX: Ref to track polling abort controller const abortControllerRef = React.useRef(null); // Helper function to calculate estimated progress based on elapsed time const calculateEstimatedProgress = (tier: string, startTime: number): number => { const elapsed = Date.now() - startTime; const duration = tier === 'enterprise' ? 90000 : 40000; // ms (90s for enterprise, 40s for professional) const linearProgress = Math.min(95, (elapsed / duration) * 100); // Logarithmic curve for natural feel - starts fast, slows down return Math.min(95, Math.round(linearProgress * (1 - Math.exp(-elapsed / 10000)))); }; const demoOptions = [ { id: 'professional', tier: 'professional', icon: Store, title: 'Panadería Artesana', subtitle: 'Tier Profesional', description: 'Panadería profesional moderna que combina calidad artesanal con eficiencia operativa', features: [ 'Gestión de inventario y stock', 'Planificación de producción', 'Gestión de pedidos y proveedores', 'Forecasting con IA (92% precisión)', 'Dashboard de métricas', 'Alertas inteligentes', 'Integración con POS', 'Recetas profesionales estandarizadas', 'Ventas omnicanal (retail + online + catering)', 'Reportes de negocio' ], characteristics: { locations: '1', employees: '12', productionModel: 'Local Artesanal', salesChannels: 'Retail + Online + Catering' }, accountType: 'professional', baseTenantId: 'a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6', color: 'primary', gradient: 'from-amber-500 to-orange-600' }, { id: 'enterprise', tier: 'enterprise', icon: Network, title: 'Cadena Enterprise', subtitle: 'Tier Enterprise', description: 'Producción centralizada con red de distribución en Madrid, Barcelona, Valencia, Sevilla y Bilbao', features: [ 'Todas las funciones Professional +', 'Gestión multi-ubicación ilimitada', 'Obrador central (Madrid) + 5 sucursales', 'Distribución y logística VRP-optimizada', 'Forecasting agregado de red', 'Dashboard enterprise consolidado', 'Transferencias internas automatizadas', 'Optimización de rutas de entrega', 'Solicitudes de procurement entre ubicaciones', 'Reportes consolidados nivel corporativo' ], characteristics: { locations: '1 obrador + 5 tiendas', employees: '45', productionModel: 'Centralizado (Madrid)', salesChannels: 'Madrid / Barcelona / Valencia / Sevilla / Bilbao' }, accountType: 'enterprise', baseTenantId: 'c3d4e5f6-a7b8-49c0-d1e2-f3a4b5c6d7e8', color: 'secondary', gradient: 'from-emerald-500 to-teal-600' } ]; const getLoadingMessage = (tier, progress) => { if (tier === 'enterprise') { if (progress < 20) return 'Iniciando tu demostración...'; if (progress < 50) return 'Creando tu panadería central...'; if (progress < 80) return 'Configurando tus sucursales...'; if (progress < 95) return 'Preparando datos finales...'; return 'Casi listo...'; } else { if (progress < 25) return 'Iniciando tu panadería...'; if (progress < 70) return 'Cargando productos y datos...'; if (progress < 95) return 'Preparando datos finales...'; return 'Casi listo...'; } }; const handleStartDemo = async (accountType, tier) => { setCreatingTier(tier); setCreationError(''); setProgressStartTime(Date.now()); setEstimatedProgress(0); try { // Start the demo session first const response = await fetch('/api/v1/demo/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('access_token') || ''}` }, body: JSON.stringify({ demo_account_type: accountType, subscription_tier: tier }) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Failed to create demo session'); } const sessionData = await response.json(); // Store demo context immediately after session creation localStorage.setItem('demo_mode', 'true'); localStorage.setItem('demo_account_type', accountType); localStorage.setItem('subscription_tier', tier); // Store tier for reference localStorage.setItem('demo_session_id', sessionData.session_id); localStorage.setItem('virtual_tenant_id', sessionData.virtual_tenant_id); localStorage.setItem('demo_expires_at', sessionData.expires_at); // BUG FIX: Store the session token as access_token for authentication if (sessionData.session_token) { console.log('🔐 [DemoPage] Storing session token:', sessionData.session_token.substring(0, 20) + '...'); localStorage.setItem('access_token', sessionData.session_token); // CRITICAL FIX: Update Zustand auth store with demo auth // This ensures the token persists across navigation and page reloads setDemoAuth(sessionData.session_token, { id: 'demo-user', email: 'demo@bakery.com', full_name: 'Demo User', is_active: true, is_verified: true, created_at: new Date().toISOString(), tenant_id: sessionData.virtual_tenant_id, }); console.log('✅ [DemoPage] Demo auth set in store'); } else { console.error('❌ [DemoPage] No session_token in response!', sessionData); } // Now poll for status until ready await pollForSessionStatus(sessionData.session_id, tier, sessionData); } catch (error) { console.error('Error creating demo:', error); setCreatingTier(null); setProgressStartTime(null); setEstimatedProgress(0); setCloneProgress({ parent: 0, children: [0, 0, 0], distribution: 0, overall: 0 }); setCreationError('Error al iniciar la demo. Por favor, inténtalo de nuevo.'); } // NOTE: State reset moved to navigation callback and error handlers // to prevent modal from disappearing before redirect }; const pollForSessionStatus = async (sessionId, tier, sessionData) => { const maxAttempts = 120; // 120 * 1s = 2 minutes max wait time let attempts = 0; // BUG-009 FIX: Create AbortController for cancellation const abortController = new AbortController(); abortControllerRef.current = abortController; // Initially show a higher progress to indicate activity setCloneProgress({ parent: 10, children: [0, 0, 0], distribution: 0, overall: 10 }); // Set up progress estimation interval for smooth UI updates const progressInterval = setInterval(() => { if (progressStartTime) { const estimated = calculateEstimatedProgress(tier, progressStartTime); setEstimatedProgress(estimated); // Update displayed progress with combination of estimated and backend setCloneProgress(prev => ({ ...prev, overall: Math.max(estimated, prev.overall) })); } }, 500); // Update every 500ms for smooth animation try { while (attempts < maxAttempts) { try { // BUG-009 FIX: Pass abort signal to fetch const statusResponse = await fetch( `/api/v1/demo/sessions/${sessionId}/status`, { signal: abortController.signal } ); if (!statusResponse.ok) { throw new Error('Failed to get session status'); } const statusData = await statusResponse.json(); // Capture estimated remaining time from backend if (statusData.estimated_remaining_seconds !== undefined) { setEstimatedRemainingSeconds(statusData.estimated_remaining_seconds); } // Update progress based on actual backend status updateProgressFromBackendStatus(statusData, tier); // BUG-010 FIX: Handle ready status separately from partial if (statusData.status === 'ready') { // Full success - set to 100% and navigate immediately clearInterval(progressInterval); setCloneProgress(prev => ({ ...prev, overall: 100 })); requestAnimationFrame(() => { // Reset state before navigation setCreatingTier(null); setProgressStartTime(null); setEstimatedProgress(0); setCloneProgress({ parent: 0, children: [0, 0, 0], distribution: 0, overall: 0 }); // Navigate to the main dashboard which will automatically route to enterprise or bakery dashboard based on subscription tier navigate('/app/dashboard'); }); return; } else if (statusData.status === 'PARTIAL' || statusData.status === 'partial') { // BUG-010 FIX: Show warning modal for partial status clearInterval(progressInterval); const failedServices = getFailedServices(statusData); setPartialWarning({ show: true, message: 'La demo está parcialmente lista. Algunos servicios no se cargaron completamente.', failedServices: failedServices, sessionData: sessionData, tier: tier }); return; } else if (statusData.status === 'FAILED' || statusData.status === 'failed') { clearInterval(progressInterval); setCreatingTier(null); setProgressStartTime(null); setEstimatedProgress(0); setCloneProgress({ parent: 0, children: [0, 0, 0], distribution: 0, overall: 0 }); setCreationError('Error al clonar los datos de demo. Por favor, inténtalo de nuevo.'); return; } // Wait before next poll - reduced from 2s to 1s await new Promise(resolve => setTimeout(resolve, 1000)); attempts++; } catch (error: any) { // BUG-009 FIX: Handle abort gracefully if (error?.name === 'AbortError') { console.log('Polling cancelled due to component unmount'); clearInterval(progressInterval); return; } throw error; } } // BUG-011 FIX: Show timeout modal instead of auto-navigating clearInterval(progressInterval); setTimeoutModal({ show: true, sessionId: sessionId, sessionData: sessionData, tier: tier }); } catch (error) { clearInterval(progressInterval); console.error('Error polling for status:', error); setCreatingTier(null); setProgressStartTime(null); setEstimatedProgress(0); setCloneProgress({ parent: 0, children: [0, 0, 0], distribution: 0, overall: 0 }); setCreationError('Error verificando el estado de la demo. Por favor, inténtalo de nuevo.'); } finally { // Clean up abort controller reference abortControllerRef.current = null; } }; // BUG-009 FIX: Cleanup on unmount React.useEffect(() => { return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); // BUG-010 FIX: Helper to extract failed services const getFailedServices = (statusData: any): string[] => { const failed: string[] = []; if (statusData.progress) { if (statusData.progress.parent?.services) { Object.entries(statusData.progress.parent.services).forEach(([name, service]: [string, any]) => { if (service?.status === 'failed') { failed.push(name); } }); } if (statusData.progress.services) { Object.entries(statusData.progress.services).forEach(([name, service]: [string, any]) => { if (service?.status === 'failed') { failed.push(name); } }); } } return failed; }; const updateProgressFromBackendStatus = (statusData, tier) => { // IMPORTANT: Backend only provides progress AFTER cloning completes // During cloning (status=PENDING), progress is empty {} // So we rely on estimated progress for visual feedback const hasRealProgress = statusData.progress && Object.keys(statusData.progress).length > 0; if (hasRealProgress) { if (tier === 'enterprise') { // Handle enterprise progress structure which may be different // Enterprise demos may have a different progress structure with parent, children, distribution handleEnterpriseProgress(statusData.progress); } else { // Handle individual bakery progress structure handleIndividualProgress(statusData.progress); } } else { // No detailed progress available - backend is still cloning // Use estimated progress for smooth visual feedback // This is NORMAL during the cloning phase setCloneProgress(prev => { const newProgress = Math.max( estimatedProgress, prev.overall // Never go backward ); // For enterprise, also update sub-components based on estimated progress if (tier === 'enterprise') { return { parent: Math.min(95, Math.round(estimatedProgress * 0.4)), // 40% weight children: [ Math.min(95, Math.round(estimatedProgress * 0.35)), Math.min(95, Math.round(estimatedProgress * 0.35)), Math.min(95, Math.round(estimatedProgress * 0.35)) ], distribution: Math.min(95, Math.round(estimatedProgress * 0.25)), // 25% weight overall: newProgress }; } return { ...prev, overall: newProgress }; }); } }; const handleIndividualProgress = (progress) => { // Count services that are completed (look for 'completed', 'ready', or 'success' status) const services = progress || {}; const completedServices = Object.values(services).filter((service: any) => service?.status === 'completed' || service?.status === 'ready' || service?.status === 'success' || service?.success === true // Some services might use success flag instead of status ).length; // Estimate overall progress based on services completed const totalServices = Object.keys(services).length || 11; // Default to 11 known services const backendProgress = Math.min(95, Math.round((completedServices / totalServices) * 100)); // Use the maximum of backend progress and estimated progress to prevent backtracking const overallProgress = Math.max(backendProgress, estimatedProgress); setCloneProgress({ parent: overallProgress, children: [0, 0, 0], distribution: 0, overall: overallProgress }); }; const handleEnterpriseProgress = (progress) => { // For enterprise demos, the progress might have a more complex structure // It may have parent, children, distribution components let backendProgress = 0; let parentProgress = 0; let childrenProgressArray = [0, 0, 0]; let distributionProgress = 0; // Check if this is the enterprise results structure (with parent/children/distribution) if (progress.parent && progress.children && progress.distribution !== undefined) { // This looks like an enterprise results structure from the end of cloning // Calculate progress based on parent, children, and distribution status // FIX 1: Handle both "completed" and "ready" for parent status const parentStatus = progress.parent.overall_status; if (parentStatus === 'ready' || parentStatus === 'completed' || parentStatus === 'partial') { parentProgress = 100; } else if (progress.parent.overall_status === 'pending') { parentProgress = 50; // Increased from 25 for better perceived progress } else { // Count parent services for more granular progress const parentServices = progress.parent.services || {}; const completedParent = Object.values(parentServices).filter((s: any) => s?.status === 'completed' || s?.status === 'ready' || s?.status === 'success' ).length; const totalParent = Object.keys(parentServices).length || 1; parentProgress = Math.round((completedParent / totalParent) * 100); } if (progress.children && progress.children.length > 0) { childrenProgressArray = progress.children.map((child: any) => { // FIX 2: Handle both status types for children const childStatus = child.status || child.overall_status; if (childStatus === 'ready' || childStatus === 'completed') return 100; if (childStatus === 'partial') return 75; if (childStatus === 'pending') return 30; return 0; }); const avgChildrenProgress = childrenProgressArray.reduce((a, b) => a + b, 0) / childrenProgressArray.length; // Ensure we have exactly 3 children values for display while (childrenProgressArray.length < 3) childrenProgressArray.push(0); childrenProgressArray = childrenProgressArray.slice(0, 3); backendProgress = Math.round((parentProgress * 0.4) + (avgChildrenProgress * 0.4)); } else { backendProgress = Math.round(parentProgress * 0.8); } if (progress.distribution) { // FIX 3: Handle both status types for distribution const distStatus = progress.distribution.status || progress.distribution.overall_status; if (distStatus === 'ready' || distStatus === 'completed') { distributionProgress = 100; } else if (distStatus === 'pending') { distributionProgress = 50; } else { distributionProgress = distStatus === 'failed' ? 100 : 75; } backendProgress = Math.round(backendProgress * 0.8 + distributionProgress * 0.2); } // FIX 4: Allow 100% progress when all components complete if (parentProgress === 100 && childrenProgressArray.every(p => p === 100) && distributionProgress === 100) { backendProgress = 100; } } else { // If it's not the enterprise result structure, fall back to service-based calculation const services = progress || {}; const completedServices = Object.values(services).filter((service: any) => service?.status === 'completed' || service?.status === 'ready' || service?.status === 'success' || service?.success === true ).length; const totalServices = Object.keys(services).length || 11; backendProgress = Math.min(95, Math.round((completedServices / totalServices) * 100)); parentProgress = backendProgress; childrenProgressArray = [backendProgress * 0.7, backendProgress * 0.7, backendProgress * 0.7]; distributionProgress = backendProgress * 0.8; } // FIX 5: Don't cap at 95% when backend reports 100% const cappedBackendProgress = backendProgress === 100 ? 100 : Math.min(95, backendProgress); const overallProgress = Math.max(cappedBackendProgress, estimatedProgress); setCloneProgress({ parent: Math.max(parentProgress, estimatedProgress * 0.9), children: childrenProgressArray, distribution: Math.max(distributionProgress, estimatedProgress * 0.7), overall: overallProgress }); }; return ( {/* Hero Section with Enhanced Design */}
{/* Background Pattern */}
{/* Animated Background Elements */}
Experiencia Demo Gratuita

Prueba Nuestra Plataforma de Gestión

Elige tu experiencia de demostración ideal y descubre cómo nuestra plataforma puede transformar tu negocio de panadería

Sin tarjeta de crédito
Configuración instantánea
Datos reales de ejemplo
{/* Main Content */}
{/* Demo Options with Improved Cards */}
{demoOptions.map((option, index) => (
setSelectedTier(option.id)} > {/* Card Header with Gradient */}

{option.title}

{option.subtitle}
{selectedTier === option.id && (
)}

{option.description}

{/* Card Body */}
{/* Features List with Icons */}

Características Incluidas

{option.features.slice(0, 6).map((feature, index) => (
{feature}
))} {option.features.length > 6 && (
+ {option.features.length - 6} funciones más
)}
{/* Characteristics Grid with Enhanced Design */}
Ubicaciones

{option.characteristics.locations}

Empleados

{option.characteristics.employees}

Producción

{option.characteristics.productionModel}

Canales

{option.characteristics.salesChannels}

{/* Card Footer */}
))}
{/* Loading Progress Modal with Enhanced Design */} {creatingTier !== null && ( { }} size="lg" >
Configurando Tu Demo
} showCloseButton={false} />
{/* Overall Progress Section */}
{getLoadingMessage(creatingTier, cloneProgress.overall)} {cloneProgress.overall}%
{estimatedRemainingSeconds !== null && estimatedRemainingSeconds > 0 && (
Aproximadamente {estimatedRemainingSeconds}s restantes
)}
{/* Information Box */}

{creatingTier === 'enterprise' ? 'Estamos preparando tu panadería con una tienda principal y 3 sucursales conectadas' : 'Estamos preparando tu panadería con productos, recetas y ventas de ejemplo'}

)} {/* Error Alert with Enhanced Design */} {creationError && (
{creationError}
)} {/* Partial Status Warning Modal */} {partialWarning?.show && ( setPartialWarning(null)} size="md" > setPartialWarning(null)} />
{partialWarning.message} {partialWarning.failedServices.length > 0 && (

Servicios con problemas:

    {partialWarning.failedServices.map((service, index) => (
  • {service}
  • ))}
)}

Puedes continuar con la demo, pero algunas funcionalidades pueden no estar disponibles. También puedes intentar crear una nueva sesión.

)} {/* Timeout Modal */} {timeoutModal?.show && ( setTimeoutModal(null)} size="md" > setTimeoutModal(null)} />
La creación de tu sesión demo está tardando más de lo habitual. Esto puede deberse a alta carga en el sistema.

Tienes las siguientes opciones:

1. Seguir Esperando

La sesión puede completarse en cualquier momento. Mantén esta página abierta.

2. Iniciar con Datos Parciales

Accede a la demo ahora con los servicios que ya estén listos.

3. Cancelar e Intentar de Nuevo

Cancela esta sesión y crea una nueva desde cero.

)}
); }; export default DemoPage;