import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button } from '../../components/ui/Button'; import { apiClient } from '../../api/client'; import { useAuthStore } from '../../stores'; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '../../components/ui/Card'; import { Badge } from '../../components/ui/Badge'; import { Alert, AlertDescription } from '../../components/ui/Alert'; import Modal, { ModalHeader, ModalBody, ModalFooter } from '../../components/ui/Modal/Modal'; import { PublicLayout } from '../../components/layout'; import { useTranslation } from 'react-i18next'; import { Store, Network, CheckCircle, Users, Building, Package, BarChart3, ForkKnife, 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 } 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); // 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' ? 75000 : 30000; // ms 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: 'blue' }, { id: 'enterprise', tier: 'enterprise', icon: Network, title: 'Cadena Enterprise', subtitle: 'Tier Enterprise', description: 'Producción centralizada con red de distribución en Madrid, Barcelona y Valencia', features: [ 'Todas las funciones Professional +', 'Gestión multi-ubicación ilimitada', 'Obrador central (Madrid) + 3 outlets', '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 + 3 tiendas', employees: '45', productionModel: 'Centralizado (Madrid)', salesChannels: 'Madrid / Barcelona / Valencia' }, accountType: 'enterprise', baseTenantId: 'c3d4e5f6-a7b8-49c0-d1e2-f3a4b5c6d7e8', color: 'purple' } ]; const getLoadingMessage = (tier, progress) => { if (tier === 'enterprise') { if (progress < 15) return 'Preparando entorno enterprise...'; if (progress < 35) return 'Creando obrador central en Madrid...'; if (progress < 55) return 'Configurando outlets en Barcelona, Valencia y Bilbao...'; if (progress < 75) return 'Generando rutas de distribución optimizadas...'; if (progress < 90) return 'Configurando red de distribución...'; return 'Finalizando configuración enterprise...'; } else { if (progress < 30) return 'Preparando tu panadería...'; if (progress < 60) return 'Configurando inventario y recetas...'; if (progress < 85) return 'Generando datos de ventas y producción...'; return 'Finalizando configuración...'; } }; 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); setCreationError('Error al iniciar la demo. Por favor, inténtalo de nuevo.'); } finally { setCreatingTier(null); setProgressStartTime(null); setEstimatedProgress(0); // Reset progress setCloneProgress({ parent: 0, children: [0, 0, 0], distribution: 0, overall: 0 }); } }; 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(); // 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 - navigate immediately clearInterval(progressInterval); setTimeout(() => { const targetUrl = tier === 'enterprise' ? `/app/tenants/${sessionData.virtual_tenant_id}/enterprise` : `/app/tenants/${sessionData.virtual_tenant_id}/dashboard`; navigate(targetUrl); }, 1000); 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); 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); 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 if (progress.parent.overall_status === 'ready' || progress.parent.overall_status === '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) => { if (child.status === 'ready' || child.status === 'completed') return 100; if (child.status === 'partial') return 75; if (child.status === '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) { if (progress.distribution.status === 'ready' || progress.distribution.status === 'completed') { distributionProgress = 100; } else if (progress.distribution.status === 'pending') { distributionProgress = 50; } else { distributionProgress = progress.distribution.status === 'failed' ? 100 : 75; } backendProgress = Math.round(backendProgress * 0.8 + distributionProgress * 0.2); } } 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; } // Use the maximum of backend progress and estimated progress to prevent backtracking const overallProgress = Math.max(Math.min(95, backendProgress), estimatedProgress); setCloneProgress({ parent: Math.max(parentProgress, estimatedProgress * 0.9), children: childrenProgressArray, distribution: Math.max(distributionProgress, estimatedProgress * 0.7), overall: overallProgress }); }; const getIconColor = (color) => { const colors = { blue: 'text-blue-600', purple: 'text-purple-600', green: 'text-green-600', orange: 'text-orange-600' }; return colors[color] || 'text-blue-600'; }; return ( {/* Hero Section */}

Prueba Nuestra Plataforma

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

{/* Main Content */}
{/* Demo Options */}
{demoOptions.map((option) => ( setSelectedTier(option.id)} >
{option.title} {option.subtitle}

{option.description}

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

{option.characteristics.locations}

Empleados:

{option.characteristics.employees}

Producción:

{option.characteristics.productionModel}

Canales:

{option.characteristics.salesChannels}

))}
{/* Loading Progress */} {creatingTier !== null && (
Configurando Tu Demo
Progreso total {cloneProgress.overall}%
{creatingTier === 'enterprise' && (
Obrador Central {cloneProgress.parent}%
{cloneProgress.children.map((progress, index) => (
Outlet {index + 1}
{progress}%
))}
Distribución {cloneProgress.distribution}%
)}
)} {/* Error Alert */} {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.

)} {/* Comparison Section */}

Comparación de Funcionalidades

Función

Professional
Individual Bakery
Enterprise
Chain of Bakeries
{[ { feature: 'Número Máximo de Ubicaciones', professional: '1', enterprise: 'Ilimitado' }, { feature: 'Gestión de Inventario', professional: '✓', enterprise: '✓ Agregado' }, { feature: 'Forecasting con IA', professional: 'Personalizado', enterprise: 'Agregado + Individual' }, { feature: 'Planificación de Producción', professional: '✓', enterprise: '✓ Centralizada' }, { feature: 'Transferencias Internas', professional: '×', enterprise: '✓ Optimizadas' }, { feature: 'Logística y Rutas', professional: '×', enterprise: '✓ Optimización VRP' }, { feature: 'Dashboard Multi-ubicación', professional: '×', enterprise: '✓ Visión de Red' }, { feature: 'Reportes Consolidados', professional: '×', enterprise: '✓ Nivel de Red' } ].map((row, index) => (
{row.feature}
{row.professional}
{row.enterprise}
))}
); }; export default DemoPage;