Files
bakery-ia/frontend/src/pages/public/DemoPage.tsx
Urtzi Alfaro bf1db7cb9e New token arch
2026-01-10 21:45:37 +01:00

1035 lines
43 KiB
TypeScript

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<string | null>(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<number | null>(null);
const [estimatedRemainingSeconds, setEstimatedRemainingSeconds] = useState<number | null>(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<AbortController | null>(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 = 5000; // ms (5s for both professional and enterprise)
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 / 1000))));
};
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,
}, tier); // NEW: Pass subscription tier to setDemoAuth
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 (
<PublicLayout
variant="default"
contentPadding="md"
headerProps={{
showThemeToggle: true,
showAuthButtons: true,
showLanguageSelector: true,
variant: "default"
}}
>
{/* Hero Section with Enhanced Design */}
<section className="relative overflow-hidden bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5 dark:from-[var(--bg-primary)] dark:via-[var(--bg-secondary)] dark:to-[var(--color-primary)]/10 py-24">
{/* Background Pattern */}
<div className="absolute inset-0 bg-pattern opacity-50"></div>
{/* Animated Background Elements */}
<div className="absolute top-20 left-10 w-72 h-72 bg-[var(--color-primary)]/10 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-10 right-10 w-96 h-96 bg-[var(--color-secondary)]/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }}></div>
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center space-y-6 animate-fade-in">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[var(--color-primary)]/10 dark:bg-[var(--color-primary)]/20 border border-[var(--color-primary)]/20 dark:border-[var(--color-primary)]/30 mb-4">
<Sparkles className="w-4 h-4 text-[var(--color-primary)]" />
<span className="text-sm font-medium text-[var(--color-primary)]">Experiencia Demo Gratuita</span>
</div>
<h1 className="text-5xl md:text-6xl font-bold text-[var(--text-primary)] mb-6 leading-tight">
Prueba Nuestra
<span className="block bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] bg-clip-text text-transparent">
Plataforma de Gestión
</span>
</h1>
<p className="text-xl md:text-2xl text-[var(--text-secondary)] max-w-3xl mx-auto leading-relaxed">
Elige tu experiencia de demostración ideal y descubre cómo nuestra plataforma puede transformar tu negocio de panadería
</p>
<div className="flex items-center justify-center gap-8 pt-4 text-sm text-[var(--text-tertiary)]">
<div className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-[var(--color-success)]" />
<span>Sin tarjeta de crédito</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-[var(--color-success)]" />
<span>Configuración instantánea</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-[var(--color-success)]" />
<span>Datos reales de ejemplo</span>
</div>
</div>
</div>
</div>
</section>
{/* Main Content */}
<section className="py-20 bg-[var(--bg-primary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Demo Options with Improved Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
{demoOptions.map((option, index) => (
<div
key={option.id}
className={`
flex flex-col
bg-[var(--bg-primary)]
border border-[var(--border-primary)]
rounded-xl
shadow-lg
overflow-hidden
transition-all duration-300
hover:shadow-2xl
cursor-pointer
${selectedTier === option.id
? 'ring-2 ring-[var(--color-primary)] shadow-xl'
: ''
}
`}
onClick={() => setSelectedTier(option.id)}
>
{/* Card Header with Gradient */}
<div className={`bg-gradient-to-r ${option.gradient} p-6`}>
<div className="flex items-start justify-between w-full text-white mb-4">
<div className="flex items-start gap-4 flex-1">
<div className="p-3 bg-white/20 backdrop-blur-sm rounded-xl">
<option.icon className="w-8 h-8 text-white" />
</div>
<div className="flex-1">
<h3 className="text-2xl font-bold text-white mb-2">
{option.title}
</h3>
<Badge
variant={option.tier === 'enterprise' ? 'secondary' : 'default'}
className="bg-white/20 backdrop-blur-sm text-white border-white/30 capitalize font-semibold"
>
{option.subtitle}
</Badge>
</div>
</div>
{selectedTier === option.id && (
<div className="animate-scale-in">
<CheckCircle className="w-6 h-6 text-white" />
</div>
)}
</div>
<p className="text-white/90 text-base leading-relaxed">
{option.description}
</p>
</div>
{/* Card Body */}
<div className="p-6 flex-1">
{/* Features List with Icons */}
<div className="space-y-3 mb-6">
<h4 className="font-semibold text-[var(--text-primary)] text-sm uppercase tracking-wide mb-4">
Características Incluidas
</h4>
{option.features.slice(0, 6).map((feature, index) => (
<div key={index} className="flex items-start gap-3 group">
<div className="flex-shrink-0 mt-0.5">
<div className="p-1 rounded-full bg-[var(--color-success)]/10 group-hover:bg-[var(--color-success)]/20 transition-colors">
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
</div>
</div>
<span className="text-sm text-[var(--text-secondary)] group-hover:text-[var(--text-primary)] transition-colors">
{feature}
</span>
</div>
))}
{option.features.length > 6 && (
<div className="flex items-start gap-3 pt-2">
<div className="flex-shrink-0 mt-0.5">
<div className="p-1 rounded-full bg-[var(--color-info)]/10">
<PlusCircle className="w-4 h-4 text-[var(--color-info)]" />
</div>
</div>
<span className="text-sm font-medium text-[var(--color-info)]">
+ {option.features.length - 6} funciones más
</span>
</div>
)}
</div>
{/* Characteristics Grid with Enhanced Design */}
<div className="grid grid-cols-2 gap-4 p-4 rounded-xl bg-gradient-to-br from-[var(--bg-secondary)] to-[var(--bg-tertiary)] border border-[var(--border-primary)]">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-[var(--text-tertiary)]">
<MapPin className="w-4 h-4" />
<span className="text-xs font-medium uppercase tracking-wide">Ubicaciones</span>
</div>
<p className="text-sm font-semibold text-[var(--text-primary)]">
{option.characteristics.locations}
</p>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-[var(--text-tertiary)]">
<Users className="w-4 h-4" />
<span className="text-xs font-medium uppercase tracking-wide">Empleados</span>
</div>
<p className="text-sm font-semibold text-[var(--text-primary)]">
{option.characteristics.employees}
</p>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-[var(--text-tertiary)]">
<Factory className="w-4 h-4" />
<span className="text-xs font-medium uppercase tracking-wide">Producción</span>
</div>
<p className="text-sm font-semibold text-[var(--text-primary)]">
{option.characteristics.productionModel}
</p>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-[var(--text-tertiary)]">
<ShoppingBag className="w-4 h-4" />
<span className="text-xs font-medium uppercase tracking-wide">Canales</span>
</div>
<p className="text-sm font-semibold text-[var(--text-primary)]">
{option.characteristics.salesChannels}
</p>
</div>
</div>
</div>
{/* Card Footer */}
<div className="px-6 py-4 bg-[var(--bg-secondary)] border-t border-[var(--border-primary)]">
<Button
onClick={(e) => {
e.stopPropagation();
handleStartDemo(option.accountType, option.tier);
}}
disabled={creatingTier !== null}
size="lg"
isFullWidth={true}
variant="gradient"
className="font-semibold group"
>
{creatingTier === option.tier ? (
<span className="flex items-center gap-3">
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white/30 border-t-white" />
<span>{getLoadingMessage(option.tier, cloneProgress.overall)}</span>
</span>
) : (
<span className="flex items-center justify-center gap-2">
<Zap className="w-5 h-5" />
<span>Iniciar Demo {option.tier === 'enterprise' ? 'Enterprise' : 'Professional'}</span>
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
</span>
)}
</Button>
</div>
</div>
))}
</div>
{/* Loading Progress Modal with Enhanced Design */}
{creatingTier !== null && (
<Modal
isOpen={creatingTier !== null}
onClose={() => { }}
size="lg"
>
<ModalHeader
title={
<div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--color-primary)]/30 border-t-[var(--color-primary)]"></div>
<span className="text-xl font-bold text-[var(--text-primary)]">
Configurando Tu Demo
</span>
</div>
}
showCloseButton={false}
/>
<ModalBody padding="xl">
<div className="space-y-8">
{/* Overall Progress Section */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-lg font-medium text-[var(--text-primary)]">
{getLoadingMessage(creatingTier, cloneProgress.overall)}
</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">
{cloneProgress.overall}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-3 overflow-hidden">
<div
className="bg-[var(--color-primary)] h-3 rounded-full transition-all duration-500"
style={{ width: `${cloneProgress.overall}%` }}
/>
</div>
{estimatedRemainingSeconds !== null && estimatedRemainingSeconds > 0 && (
<div className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
<Clock className="w-4 h-4" />
<span>Aproximadamente {estimatedRemainingSeconds}s restantes</span>
</div>
)}
</div>
{/* Information Box */}
<div className="rounded-lg p-4 bg-[var(--bg-secondary)] border border-[var(--border-primary)]">
<p className="text-sm text-[var(--text-secondary)]">
{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'}
</p>
</div>
</div>
</ModalBody>
</Modal>
)}
{/* Error Alert with Enhanced Design */}
{creationError && (
<div className="max-w-md mx-auto mt-6 animate-shake">
<Alert variant="destructive" className="shadow-lg border-2">
<AlertDescription className="flex items-center gap-2">
<span className="flex-shrink-0 w-2 h-2 bg-[var(--color-error)] rounded-full animate-pulse"></span>
{creationError}
</AlertDescription>
</Alert>
</div>
)}
{/* Partial Status Warning Modal */}
{partialWarning?.show && (
<Modal
isOpen={partialWarning.show}
onClose={() => setPartialWarning(null)}
size="md"
>
<ModalHeader
title="Demo Parcialmente Lista"
showCloseButton={true}
onClose={() => setPartialWarning(null)}
/>
<ModalBody padding="lg">
<div className="space-y-4">
<Alert variant="warning" className="mb-4">
<AlertDescription>
{partialWarning.message}
</AlertDescription>
</Alert>
{partialWarning.failedServices.length > 0 && (
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg">
<h4 className="font-semibold text-sm mb-2 text-yellow-900 dark:text-yellow-100">
Servicios con problemas:
</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-yellow-800 dark:text-yellow-200">
{partialWarning.failedServices.map((service, index) => (
<li key={index}>{service}</li>
))}
</ul>
</div>
)}
<p className="text-sm text-[var(--text-secondary)]">
Puedes continuar con la demo, pero algunas funcionalidades pueden no estar disponibles.
También puedes intentar crear una nueva sesión.
</p>
</div>
</ModalBody>
<ModalFooter justify="end">
<div className="flex gap-3">
<Button
variant="outline"
onClick={() => {
setPartialWarning(null);
setCreatingTier(null);
setCreationError('Creación cancelada. Por favor, intenta de nuevo.');
}}
>
Cancelar e Intentar de Nuevo
</Button>
<Button
variant="primary"
onClick={() => {
// Navigate to the main dashboard which will automatically route to enterprise or bakery dashboard based on subscription tier
setPartialWarning(null);
navigate('/app/dashboard');
}}
>
Continuar con Demo Parcial
</Button>
</div>
</ModalFooter>
</Modal>
)}
{/* Timeout Modal */}
{timeoutModal?.show && (
<Modal
isOpen={timeoutModal.show}
onClose={() => setTimeoutModal(null)}
size="md"
>
<ModalHeader
title="La Configuración está Tomando más Tiempo de lo Esperado"
showCloseButton={true}
onClose={() => setTimeoutModal(null)}
/>
<ModalBody padding="lg">
<div className="space-y-4">
<Alert variant="warning" className="mb-4">
<AlertDescription>
La creación de tu sesión demo está tardando más de lo habitual.
Esto puede deberse a alta carga en el sistema.
</AlertDescription>
</Alert>
<p className="text-sm text-[var(--text-secondary)]">
Tienes las siguientes opciones:
</p>
<div className="space-y-3">
<div className="bg-[var(--color-info)]/10 dark:bg-[var(--color-info)]/20 p-3 rounded-lg">
<h4 className="font-semibold text-sm text-[var(--color-info-dark)] dark:text-[var(--color-info-light)]">
1. Seguir Esperando
</h4>
<p className="text-xs text-[var(--color-info)] dark:text-[var(--color-info-light)] mt-1">
La sesión puede completarse en cualquier momento. Mantén esta página abierta.
</p>
</div>
<div className="bg-[var(--color-success)]/10 dark:bg-[var(--color-success)]/20 p-3 rounded-lg">
<h4 className="font-semibold text-sm text-[var(--color-success-dark)] dark:text-[var(--color-success-light)]">
2. Iniciar con Datos Parciales
</h4>
<p className="text-xs text-[var(--color-success)] dark:text-[var(--color-success-light)] mt-1">
Accede a la demo ahora con los servicios que ya estén listos.
</p>
</div>
<div className="bg-[var(--bg-secondary)] dark:bg-[var(--bg-tertiary)] p-3 rounded-lg">
<h4 className="font-semibold text-sm text-[var(--text-primary)] dark:text-[var(--text-primary)]">
3. Cancelar e Intentar de Nuevo
</h4>
<p className="text-xs text-[var(--text-secondary)] dark:text-[var(--text-secondary)] mt-1">
Cancela esta sesión y crea una nueva desde cero.
</p>
</div>
</div>
</div>
</ModalBody>
<ModalFooter justify="center">
<div className="flex flex-col gap-2 w-full max-w-sm">
<Button
variant="primary"
onClick={() => {
setTimeoutModal(null);
// Continue polling
}}
>
Seguir Esperando
</Button>
<Button
variant="secondary"
onClick={() => {
// Navigate to the main dashboard which will automatically route to enterprise or bakery dashboard based on subscription tier
setTimeoutModal(null);
navigate('/app/dashboard');
}}
>
Iniciar con Datos Parciales
</Button>
<Button
variant="outline"
onClick={() => {
setTimeoutModal(null);
setCreatingTier(null);
setCreationError('Tiempo de espera excedido. Por favor, intenta de nuevo.');
}}
>
Cancelar e Intentar de Nuevo
</Button>
</div>
</ModalFooter>
</Modal>
)}
</div>
</section>
</PublicLayout>
);
};
export default DemoPage;