Files
bakery-ia/frontend/src/pages/public/DemoPage.tsx

914 lines
36 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '../../components/ui/Button';
2025-10-03 14:09:34 +02:00
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,
2025-12-05 20:07:01 +01:00
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<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);
// 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 = 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))));
};
2025-10-03 14:09:34 +02:00
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') {
2025-11-30 09:12:40 +01:00
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 {
2025-11-30 09:12:40 +01:00
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);
2025-10-03 14:09:34 +02:00
2025-10-17 18:14:28 +02:00
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');
2025-10-17 18:14:28 +02:00
} else {
console.error('❌ [DemoPage] No session_token in response!', sessionData);
2025-10-17 18:14:28 +02:00
}
// 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(() => {
2025-12-05 20:07:01 +01:00
// Navigate to the main dashboard which will automatically route to enterprise or bakery dashboard based on subscription tier
navigate('/app/dashboard');
}, 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;
}
2025-10-17 18:14:28 +02:00
}
// 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;
2025-10-17 18:14:28 +02:00
}
};
// BUG-009 FIX: Cleanup on unmount
React.useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
2025-10-17 18:14:28 +02:00
}, []);
// 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;
};
2025-10-03 14:09:34 +02:00
const updateProgressFromBackendStatus = (statusData, tier) => {
2025-11-30 09:12:40 +01:00
// 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 {
2025-11-30 09:12:40 +01:00
// 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,
2025-11-30 09:12:40 +01:00
prev.overall // Never go backward
);
2025-11-30 09:12:40 +01:00
// 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
};
2025-10-03 14:09:34 +02:00
});
}
};
2025-10-03 14:09:34 +02:00
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);
}
2025-10-03 14:09:34 +02:00
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;
2025-10-17 18:14:28 +02:00
}
backendProgress = Math.round(backendProgress * 0.8 + distributionProgress * 0.2);
2025-10-17 18:14:28 +02:00
}
} 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;
2025-10-03 14:09:34 +02:00
}
// 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
});
2025-10-03 14:09:34 +02:00
};
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';
};
2025-10-03 14:09:34 +02:00
return (
<PublicLayout
variant="default"
contentPadding="md"
2025-10-03 14:09:34 +02:00
headerProps={{
showThemeToggle: true,
showAuthButtons: true,
showLanguageSelector: true,
variant: "default"
2025-10-03 14:09:34 +02:00
}}
>
{/* Hero Section */}
<section className="bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5 py-20">
2025-10-03 14:09:34 +02:00
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h1 className="text-4xl font-bold text-[var(--text-primary)] mb-4">
Prueba Nuestra Plataforma
2025-10-03 14:09:34 +02:00
</h1>
<p className="text-xl text-[var(--text-secondary)] max-w-3xl mx-auto">
Elige tu experiencia de demostración ideal y explora cómo nuestra
plataforma puede transformar tu negocio de panadería
2025-10-03 14:09:34 +02:00
</p>
</div>
</div>
</section>
2025-10-03 14:09:34 +02:00
{/* Main Content */}
<section className="py-16 bg-[var(--bg-primary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Demo Options */}
<div className="grid md:grid-cols-2 gap-8 mb-12">
{demoOptions.map((option) => (
<Card
key={option.id}
2025-12-05 20:07:01 +01:00
className={`cursor-pointer hover:shadow-xl transition-all border-2 ${selectedTier === option.id ? 'border-primary bg-primary/5' : 'border-gray-200 dark:border-gray-700'
}`}
onClick={() => setSelectedTier(option.id)}
>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<option.icon className={`w-12 h-12 ${getIconColor(option.color)} text-[var(--color-primary)]`} />
<div>
<CardTitle className="text-2xl text-[var(--text-primary)]">{option.title}</CardTitle>
<Badge
variant={option.tier === 'enterprise' ? 'premium' : 'default'}
className="mt-2 capitalize"
>
{option.subtitle}
</Badge>
2025-10-03 14:09:34 +02:00
</div>
</div>
</div>
<p className="text-[var(--text-secondary)] mt-4">{option.description}</p>
</CardHeader>
<CardContent>
{/* Features List */}
<div className="space-y-3 mb-6">
{option.features.slice(0, 6).map((feature, index) => (
<div key={index} className="flex items-start gap-3">
<CheckCircle className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<span className="text-sm text-[var(--text-secondary)]">{feature}</span>
</div>
))}
{option.features.length > 6 && (
<div className="flex items-start gap-3 text-sm text-[var(--text-tertiary)]">
<PlusCircle className="w-5 h-5 flex-shrink-0 mt-0.5" />
<span>+ {option.features.length - 6} funciones más</span>
</div>
)}
</div>
2025-10-03 14:09:34 +02:00
{/* Characteristics Grid */}
<div className="grid grid-cols-2 gap-4 text-sm border-t border-[var(--border-primary)] pt-4">
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-[var(--text-tertiary)]" />
<div>
<span className="font-semibold text-[var(--text-primary)]">Ubicaciones:</span>
<p className="text-[var(--text-secondary)]">{option.characteristics.locations}</p>
2025-11-01 21:35:03 +01:00
</div>
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-[var(--text-tertiary)]" />
<div>
<span className="font-semibold text-[var(--text-primary)]">Empleados:</span>
<p className="text-[var(--text-secondary)]">{option.characteristics.employees}</p>
</div>
</div>
<div className="flex items-center gap-2">
<Factory className="w-4 h-4 text-[var(--text-tertiary)]" />
<div>
<span className="font-semibold text-[var(--text-primary)]">Producción:</span>
<p className="text-[var(--text-secondary)]">{option.characteristics.productionModel}</p>
</div>
</div>
<div className="flex items-center gap-2">
<ShoppingBag className="w-4 h-4 text-[var(--text-tertiary)]" />
<div>
<span className="font-semibold text-[var(--text-primary)]">Canales:</span>
<p className="text-[var(--text-secondary)]">{option.characteristics.salesChannels}</p>
2025-10-03 14:09:34 +02:00
</div>
</div>
</div>
</CardContent>
<CardFooter>
<Button
onClick={() => handleStartDemo(option.accountType, option.tier)}
disabled={creatingTier !== null}
className="w-full h-12 text-lg font-semibold"
variant={option.tier === 'enterprise' ? 'premium' : 'default'}
>
{creatingTier === option.tier ? (
<div className="flex items-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
{getLoadingMessage(option.tier, cloneProgress.overall)}
</div>
) : (
<>
<span>Iniciar Demo {option.tier === 'enterprise' ? 'Enterprise' : 'Professional'}</span>
<ArrowRight className="ml-2 w-4 h-4" />
</>
2025-10-03 14:09:34 +02:00
)}
</Button>
</CardFooter>
</Card>
))}
</div>
2025-10-03 14:09:34 +02:00
2025-12-05 20:07:01 +01:00
{/* Loading Progress Modal */}
{creatingTier !== null && (
2025-12-05 20:07:01 +01:00
<Modal
isOpen={creatingTier !== null}
onClose={() => { }}
size="md"
>
<ModalHeader
title="Configurando Tu Demo"
showCloseButton={false}
/>
<ModalBody padding="lg">
<div className="space-y-4">
<div className="flex justify-between text-sm">
<span>Progreso total</span>
<span>{cloneProgress.overall}%</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{ width: `${cloneProgress.overall}%` }}
></div>
</div>
<div className="text-center text-sm text-[var(--text-secondary)] mt-4">
{getLoadingMessage(creatingTier, cloneProgress.overall)}
</div>
2025-12-05 20:07:01 +01:00
{creatingTier === 'enterprise' && (
<div className="space-y-3 mt-4">
<div className="flex justify-between text-sm">
<span className="font-medium">Obrador Central</span>
<span>{cloneProgress.parent}%</span>
</div>
<div className="grid grid-cols-3 gap-3">
{cloneProgress.children.map((progress, index) => (
<div key={index} className="text-center">
<div className="text-xs text-[var(--text-tertiary)] mb-1">Outlet {index + 1}</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
></div>
</div>
2025-12-05 20:07:01 +01:00
<div className="text-xs mt-1">{progress}%</div>
</div>
))}
</div>
2025-12-05 20:07:01 +01:00
<div className="flex justify-between text-sm mt-2">
<span className="font-medium">Distribución</span>
<span>{cloneProgress.distribution}%</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
<div
className="bg-purple-500 h-1.5 rounded-full transition-all duration-300"
style={{ width: `${cloneProgress.distribution}%` }}
></div>
</div>
</div>
)}
</div>
</ModalBody>
</Modal>
)}
2025-10-03 14:09:34 +02:00
{/* Error Alert */}
{creationError && (
<Alert variant="destructive" className="max-w-md mx-auto mt-4">
<AlertDescription>{creationError}</AlertDescription>
</Alert>
)}
{/* Partial Status Warning Modal */}
{partialWarning?.show && (
<Modal
isOpen={partialWarning.show}
onClose={() => setPartialWarning(null)}
size="md"
2025-10-03 14:09:34 +02:00
>
<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>
)}
2025-10-17 18:14:28 +02:00
<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>
2025-10-17 18:14:28 +02:00
</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={() => {
2025-12-05 20:07:01 +01:00
// Navigate to the main dashboard which will automatically route to enterprise or bakery dashboard based on subscription tier
setPartialWarning(null);
2025-12-05 20:07:01 +01:00
navigate('/app/dashboard');
}}
>
Continuar con Demo Parcial
</Button>
</div>
</ModalFooter>
</Modal>
)}
2025-10-17 18:14:28 +02:00
{/* 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-blue-50 dark:bg-blue-900/20 p-3 rounded-lg">
<h4 className="font-semibold text-sm text-blue-900 dark:text-blue-100">
1. Seguir Esperando
</h4>
<p className="text-xs text-blue-800 dark:text-blue-200 mt-1">
La sesión puede completarse en cualquier momento. Mantén esta página abierta.
</p>
</div>
2025-10-17 18:14:28 +02:00
<div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-lg">
<h4 className="font-semibold text-sm text-green-900 dark:text-green-100">
2. Iniciar con Datos Parciales
</h4>
<p className="text-xs text-green-800 dark:text-green-200 mt-1">
Accede a la demo ahora con los servicios que ya estén listos.
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-lg">
<h4 className="font-semibold text-sm text-gray-900 dark:text-gray-100">
3. Cancelar e Intentar de Nuevo
</h4>
<p className="text-xs text-gray-800 dark:text-gray-200 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={() => {
2025-12-05 20:07:01 +01:00
// Navigate to the main dashboard which will automatically route to enterprise or bakery dashboard based on subscription tier
setTimeoutModal(null);
2025-12-05 20:07:01 +01:00
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>
2025-10-17 18:14:28 +02:00
</div>
</ModalFooter>
</Modal>
)}
2025-10-17 18:14:28 +02:00
</div>
</section>
2025-10-03 14:09:34 +02:00
</PublicLayout>
);
};
export default DemoPage;