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

483 lines
24 KiB
TypeScript
Raw Normal View History

2025-10-17 18:14:28 +02:00
import React, { useState, useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom';
2025-11-19 07:46:40 +01:00
import { useTranslation } from 'react-i18next';
2025-10-03 14:09:34 +02:00
import { PublicLayout } from '../../components/layout';
import { Button } from '../../components/ui';
2025-10-17 18:14:28 +02:00
import { getDemoAccounts, createDemoSession, DemoAccount, demoSessionAPI } from '../../api/services/demo';
2025-10-03 14:09:34 +02:00
import { apiClient } from '../../api/client';
2025-10-17 18:14:28 +02:00
import { Check, Clock, Shield, Play, Zap, ArrowRight, Store, Factory, Loader2 } from 'lucide-react';
import { markTourAsStartPending } from '../../features/demo-onboarding';
2025-10-03 14:09:34 +02:00
2025-10-17 18:14:28 +02:00
const POLL_INTERVAL_MS = 1500; // Poll every 1.5 seconds
2025-10-03 14:09:34 +02:00
export const DemoPage: React.FC = () => {
2025-11-19 07:46:40 +01:00
const { t } = useTranslation();
2025-10-03 14:09:34 +02:00
const [demoAccounts, setDemoAccounts] = useState<DemoAccount[]>([]);
const [loading, setLoading] = useState(true);
const [creatingSession, setCreatingSession] = useState(false);
const [error, setError] = useState<string | null>(null);
2025-10-17 18:14:28 +02:00
const [progressPercentage, setProgressPercentage] = useState(0);
const [estimatedTime, setEstimatedTime] = useState(5);
2025-10-03 14:09:34 +02:00
useEffect(() => {
const fetchDemoAccounts = async () => {
try {
const accounts = await getDemoAccounts();
setDemoAccounts(accounts);
} catch (err) {
2025-11-19 07:46:40 +01:00
setError(t('demo:errors.loading_accounts', 'Error al cargar las cuentas demo'));
2025-10-03 14:09:34 +02:00
console.error('Error fetching demo accounts:', err);
} finally {
setLoading(false);
}
};
fetchDemoAccounts();
}, []);
2025-10-17 18:14:28 +02:00
const pollStatus = useCallback(async (sessionId: string) => {
try {
const statusData = await demoSessionAPI.getSessionStatus(sessionId);
// Calculate progress - ALWAYS update, even if no progress data yet
if (statusData.progress && Object.keys(statusData.progress).length > 0) {
const services = Object.values(statusData.progress);
const totalServices = services.length;
if (totalServices > 0) {
const completedServices = services.filter(
(s) => s.status === 'completed' || s.status === 'failed'
).length;
const percentage = Math.round((completedServices / totalServices) * 100);
setProgressPercentage(percentage);
// Estimate remaining time
const remainingServices = totalServices - completedServices;
setEstimatedTime(Math.max(remainingServices * 2, 1));
} else {
// No services yet, show minimal progress
setProgressPercentage(5);
}
} else {
// No progress data yet, show initial state
setProgressPercentage(10);
}
// Check if ready to redirect
// CRITICAL: Wait for inventory, recipes, AND suppliers to complete
// to prevent dashboard from showing SetupWizardBlocker
const progress = statusData.progress || {};
const inventoryReady = progress.inventory?.status === 'completed';
const recipesReady = progress.recipes?.status === 'completed';
const suppliersReady = progress.suppliers?.status === 'completed';
const criticalServicesReady = inventoryReady && recipesReady && suppliersReady;
// Additionally verify that we have minimum required data to bypass SetupWizardBlocker
// The SetupWizardBlocker requires: 3+ ingredients, 1+ suppliers, 1+ recipes
// Ensure progress data exists for all required services
const hasInventoryProgress = !!progress.inventory;
const hasSuppliersProgress = !!progress.suppliers;
const hasRecipesProgress = !!progress.recipes;
// Extract counts with defensive checks
const ingredientsCount = hasInventoryProgress ? (progress.inventory.details?.ingredients || 0) : 0;
const suppliersCount = hasSuppliersProgress ? (progress.suppliers.details?.suppliers || 0) : 0;
const recipesCount = hasRecipesProgress ? (progress.recipes.details?.recipes || 0) : 0;
// Verify we have the minimum required counts
const hasMinimumIngredients = (typeof ingredientsCount === 'number' && ingredientsCount >= 3);
const hasMinimumSuppliers = (typeof suppliersCount === 'number' && suppliersCount >= 1);
const hasMinimumRecipes = (typeof recipesCount === 'number' && recipesCount >= 1);
// Ensure all required services have completed AND we have minimum data
const hasMinimumRequiredData =
hasInventoryProgress &&
hasSuppliersProgress &&
hasRecipesProgress &&
hasMinimumIngredients &&
hasMinimumSuppliers &&
hasMinimumRecipes;
2025-10-17 18:14:28 +02:00
const shouldRedirect =
(statusData.status === 'ready' && hasMinimumRequiredData) || // Ready status AND minimum required data
(criticalServicesReady && hasMinimumRequiredData); // Critical services done + minimum required data
2025-10-17 18:14:28 +02:00
if (shouldRedirect) {
// Show 100% before redirect
setProgressPercentage(100);
// Small delay for smooth transition
setTimeout(() => {
window.location.href = `/app/dashboard?session=${sessionId}`;
}, 300);
return true; // Stop polling
}
return false; // Continue polling
} catch (err) {
console.error('Error polling session status:', err);
return false;
}
}, []);
2025-10-03 14:09:34 +02:00
const handleStartDemo = async (accountType: string) => {
setCreatingSession(true);
setError(null);
2025-10-17 18:14:28 +02:00
setProgressPercentage(0);
setEstimatedTime(6);
2025-10-03 14:09:34 +02:00
try {
const session = await createDemoSession({
demo_account_type: accountType as 'individual_bakery' | 'central_baker',
});
console.log('✅ Demo session created:', session);
2025-10-03 14:09:34 +02:00
// Store session ID in API client
apiClient.setDemoSessionId(session.session_id);
2025-10-17 18:14:28 +02:00
// Set the virtual tenant ID in API client
apiClient.setTenantId(session.virtual_tenant_id);
console.log('✅ Set API client tenant ID:', session.virtual_tenant_id);
2025-10-03 14:09:34 +02:00
// Store session info in localStorage for UI
localStorage.setItem('demo_mode', 'true');
localStorage.setItem('demo_session_id', session.session_id);
localStorage.setItem('demo_account_type', accountType);
localStorage.setItem('demo_expires_at', session.expires_at);
localStorage.setItem('demo_tenant_id', session.virtual_tenant_id);
2025-10-17 18:14:28 +02:00
// Start polling IMMEDIATELY in parallel with other setup
const pollInterval = setInterval(async () => {
const shouldStop = await pollStatus(session.session_id);
if (shouldStop) {
clearInterval(pollInterval);
}
}, POLL_INTERVAL_MS);
// Initialize tenant store and other setup in parallel (non-blocking)
Promise.all([
import('../../stores/tenant.store').then(({ useTenantStore }) => {
const demoTenant = {
id: session.virtual_tenant_id,
name: session.demo_config?.name || `Demo ${accountType}`,
business_type: accountType === 'individual_bakery' ? 'bakery' : 'central_baker',
business_model: accountType,
address: session.demo_config?.address || 'Demo Address',
city: session.demo_config?.city || 'Madrid',
postal_code: '28001',
phone: null,
is_active: true,
subscription_tier: 'demo',
ml_model_trained: false,
last_training_date: null,
owner_id: 'demo-user',
created_at: new Date().toISOString(),
};
useTenantStore.getState().setCurrentTenant(demoTenant);
console.log('✅ Initialized tenant store with demo tenant:', demoTenant);
}),
// Mark tour to start automatically
Promise.resolve(markTourAsStartPending()),
]).catch(err => console.error('Error initializing tenant store:', err));
// Initial poll (don't wait for tenant store)
const shouldStop = await pollStatus(session.session_id);
if (shouldStop) {
clearInterval(pollInterval);
}
2025-10-03 14:09:34 +02:00
} catch (err: any) {
2025-11-19 07:46:40 +01:00
setError(err?.message || t('demo:errors.creating_session', 'Error al crear sesión demo'));
2025-10-03 14:09:34 +02:00
console.error('Error creating demo session:', err);
setCreatingSession(false);
}
};
const getAccountIcon = (accountType: string) => {
return accountType === 'individual_bakery' ? Store : Factory;
};
if (loading) {
return (
<PublicLayout
variant="full-width"
contentPadding="none"
headerProps={{
showThemeToggle: true,
showAuthButtons: true,
showLanguageSelector: true,
}}
>
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[var(--color-primary)] mx-auto"></div>
2025-11-19 07:46:40 +01:00
<p className="mt-4 text-[var(--text-secondary)]">{t('demo:loading.initial', 'Cargando cuentas demo...')}</p>
2025-10-03 14:09:34 +02:00
</div>
</div>
</PublicLayout>
);
}
return (
<PublicLayout
variant="full-width"
contentPadding="none"
headerProps={{
showThemeToggle: true,
showAuthButtons: true,
showLanguageSelector: true,
}}
>
{/* Hero Section */}
<section className="relative py-20 lg:py-32 bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<div className="mb-6">
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
<Play className="w-4 h-4 mr-2" />
2025-11-19 07:46:40 +01:00
{t('demo:hero.badge', 'Demo Interactiva')}
2025-10-03 14:09:34 +02:00
</span>
</div>
<h1 className="text-4xl tracking-tight font-extrabold text-[var(--text-primary)] sm:text-5xl lg:text-6xl">
2025-11-19 07:46:40 +01:00
<span className="block">{t('demo:hero.title', 'Prueba El Panadero Digital')}</span>
<span className="block text-[var(--color-primary)]">{t('demo:hero.subtitle', 'sin compromiso')}</span>
2025-10-03 14:09:34 +02:00
</h1>
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)] sm:text-xl">
2025-11-19 07:46:40 +01:00
{t('demo:hero.description', 'Elige el tipo de panadería que se ajuste a tu negocio')}
2025-10-03 14:09:34 +02:00
</p>
<div className="mt-8 flex items-center justify-center space-x-6 text-sm text-[var(--text-tertiary)]">
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
2025-11-19 07:46:40 +01:00
{t('demo:hero.benefits.no_credit_card', 'Sin tarjeta de crédito')}
2025-10-03 14:09:34 +02:00
</div>
<div className="flex items-center">
<Clock className="w-4 h-4 text-green-500 mr-2" />
2025-11-19 07:46:40 +01:00
{t('demo:hero.benefits.access_time', '30 minutos de acceso')}
2025-10-03 14:09:34 +02:00
</div>
<div className="flex items-center">
<Shield className="w-4 h-4 text-green-500 mr-2" />
2025-11-19 07:46:40 +01:00
{t('demo:hero.benefits.real_data', 'Datos reales en español')}
2025-10-03 14:09:34 +02:00
</div>
</div>
</div>
{error && (
<div className="mb-8 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-lg max-w-2xl mx-auto">
{error}
</div>
)}
{/* Demo Account Cards */}
<div className="grid md:grid-cols-2 gap-8 max-w-6xl mx-auto">
{demoAccounts.map((account) => {
const Icon = getAccountIcon(account.account_type);
return (
<div
key={account.account_type}
className="relative bg-[var(--bg-primary)] rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden border border-[var(--border-default)] group"
>
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-br from-[var(--color-primary)]/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative p-8">
{/* Header */}
<div className="flex items-start justify-between mb-6">
<div className="flex items-center">
<div className="p-3 rounded-xl bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
<Icon className="w-6 h-6" />
</div>
<div className="ml-4">
<h2 className="text-2xl font-bold text-[var(--text-primary)]">
2025-11-01 21:35:03 +01:00
{account.account_type === 'individual_bakery'
2025-11-19 07:46:40 +01:00
? t('demo:accounts.individual_bakery.title', 'Panadería Individual con Producción local')
: t('demo:accounts.central_baker.title', 'Panadería Franquiciada con Obrador Central')}
2025-10-03 14:09:34 +02:00
</h2>
<p className="text-sm text-[var(--text-tertiary)] mt-1">
2025-11-01 21:35:03 +01:00
{account.account_type === 'individual_bakery'
2025-11-19 07:46:40 +01:00
? t('demo:accounts.individual_bakery.subtitle', account.business_model)
: t('demo:accounts.central_baker.subtitle', 'Punto de Venta + Obrador Central')}
2025-10-03 14:09:34 +02:00
</p>
</div>
</div>
<span className="px-3 py-1 bg-[var(--color-primary)]/10 text-[var(--color-primary)] rounded-full text-xs font-semibold">
2025-11-19 07:46:40 +01:00
{t('demo:accounts.demo_badge', 'DEMO')}
2025-10-03 14:09:34 +02:00
</span>
</div>
{/* Description */}
<p className="text-[var(--text-secondary)] mb-6">
{account.description}
</p>
2025-11-01 21:35:03 +01:00
{/* Key Characteristics */}
<div className="mb-6 p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-default)]">
<p className="text-xs font-semibold text-[var(--text-tertiary)] uppercase mb-3">
2025-11-19 07:46:40 +01:00
{account.account_type === 'individual_bakery'
? t('demo:accounts.individual_bakery.characteristics.title', 'Características del negocio')
: t('demo:accounts.central_baker.characteristics.title', 'Características del negocio')}
2025-11-01 21:35:03 +01:00
</p>
<div className="grid grid-cols-2 gap-3 text-sm">
{account.account_type === 'individual_bakery' ? (
<>
<div>
2025-11-19 07:46:40 +01:00
<span className="text-[var(--text-tertiary)]">{t('demo:accounts.individual_bakery.characteristics.employees', 'Empleados')}:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">{t('demo:accounts.individual_bakery.characteristics.employees_value', '~8')}</span>
2025-11-01 21:35:03 +01:00
</div>
<div>
2025-11-19 07:46:40 +01:00
<span className="text-[var(--text-tertiary)]">{t('demo:accounts.individual_bakery.characteristics.shifts', 'Turnos')}:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">{t('demo:accounts.individual_bakery.characteristics.shifts_value', '1/día')}</span>
2025-11-01 21:35:03 +01:00
</div>
<div>
2025-11-19 07:46:40 +01:00
<span className="text-[var(--text-tertiary)]">{t('demo:accounts.individual_bakery.characteristics.sales', 'Ventas')}:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">{t('demo:accounts.individual_bakery.characteristics.sales_value', 'Directas')}</span>
2025-11-01 21:35:03 +01:00
</div>
<div>
2025-11-19 07:46:40 +01:00
<span className="text-[var(--text-tertiary)]">{t('demo:accounts.individual_bakery.characteristics.products', 'Productos')}:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">{t('demo:accounts.individual_bakery.characteristics.products_value', 'Local')}</span>
2025-11-01 21:35:03 +01:00
</div>
</>
) : (
<>
<div>
2025-11-19 07:46:40 +01:00
<span className="text-[var(--text-tertiary)]">{t('demo:accounts.central_baker.characteristics.employees', 'Empleados')}:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">{t('demo:accounts.central_baker.characteristics.employees_value', '~5-6')}</span>
2025-11-01 21:35:03 +01:00
</div>
<div>
2025-11-19 07:46:40 +01:00
<span className="text-[var(--text-tertiary)]">{t('demo:accounts.central_baker.characteristics.shifts', 'Turnos')}:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">{t('demo:accounts.central_baker.characteristics.shifts_value', '2/día')}</span>
2025-11-01 21:35:03 +01:00
</div>
<div>
2025-11-19 07:46:40 +01:00
<span className="text-[var(--text-tertiary)]">{t('demo:accounts.central_baker.characteristics.model', 'Modelo')}:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">{t('demo:accounts.central_baker.characteristics.model_value', 'Franquicia')}</span>
2025-11-01 21:35:03 +01:00
</div>
<div>
2025-11-19 07:46:40 +01:00
<span className="text-[var(--text-tertiary)]">{t('demo:accounts.central_baker.characteristics.products', 'Productos')}:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">{t('demo:accounts.central_baker.characteristics.products_value', 'De obrador')}</span>
2025-11-01 21:35:03 +01:00
</div>
</>
)}
</div>
</div>
2025-10-03 14:09:34 +02:00
{/* Features */}
{account.features && account.features.length > 0 && (
2025-11-01 21:35:03 +01:00
<div className="mb-8 space-y-2">
2025-10-03 14:09:34 +02:00
<p className="text-sm font-semibold text-[var(--text-primary)] mb-3">
2025-11-19 07:46:40 +01:00
{t('demo:accounts.features_title', 'Funcionalidades incluidas:')}
2025-10-03 14:09:34 +02:00
</p>
{account.features.map((feature, idx) => (
<div key={idx} className="flex items-center text-sm text-[var(--text-secondary)]">
<Zap className="w-4 h-4 mr-2 text-[var(--color-primary)]" />
{feature}
</div>
))}
</div>
)}
{/* CTA Button */}
<Button
onClick={() => handleStartDemo(account.account_type)}
disabled={creatingSession}
size="lg"
2025-11-01 21:35:03 +01:00
className="w-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] hover:from-[var(--color-primary-dark)] hover:to-[var(--color-primary)] text-white shadow-lg hover:shadow-2xl transform hover:scale-[1.02] transition-all duration-200 font-semibold text-base py-4"
2025-10-03 14:09:34 +02:00
>
2025-10-17 18:14:28 +02:00
<Play className="mr-2 w-5 h-5" />
2025-11-19 07:46:40 +01:00
{t('demo:accounts.start_demo', 'Iniciar Demo')}
2025-11-01 21:35:03 +01:00
<ArrowRight className="ml-2 w-5 h-5" />
2025-10-03 14:09:34 +02:00
</Button>
</div>
</div>
);
})}
</div>
{/* Footer CTA */}
<div className="mt-16 text-center">
<p className="text-[var(--text-secondary)] mb-4">
2025-11-19 07:46:40 +01:00
{t('demo:footer.have_account', '¿Ya tienes una cuenta?')}
2025-10-03 14:09:34 +02:00
</p>
<Link
to="/login"
className="inline-flex items-center text-[var(--color-primary)] hover:text-[var(--color-primary-dark)] font-semibold transition-colors"
>
2025-11-19 07:46:40 +01:00
{t('demo:footer.login_link', 'Inicia sesión aquí')}
2025-10-03 14:09:34 +02:00
<ArrowRight className="ml-2 w-4 h-4" />
</Link>
</div>
</div>
</section>
2025-10-17 18:14:28 +02:00
{/* Loading Modal Overlay */}
{creatingSession && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-[var(--bg-primary)] rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4 border border-[var(--border-default)]">
<div className="text-center">
{/* Animated loader */}
<div className="mb-6 flex justify-center">
<div className="relative w-20 h-20">
<Loader2 className="w-20 h-20 text-[var(--color-primary)] animate-spin" />
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-xl font-bold text-[var(--color-primary)]">
{Math.min(progressPercentage, 100)}%
</span>
</div>
</div>
</div>
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-2">
2025-11-19 07:46:40 +01:00
{progressPercentage >= 100
? t('demo:loading.ready_title', '¡Listo! Redirigiendo...')
: t('demo:loading.preparing_title', 'Preparando tu Demo')}
2025-10-17 18:14:28 +02:00
</h2>
<p className="text-[var(--text-secondary)] mb-6">
{progressPercentage >= 100
2025-11-19 07:46:40 +01:00
? t('demo:loading.ready_description', 'Tu entorno está listo. Accediendo al dashboard...')
: t('demo:loading.preparing_description', 'Configurando tu entorno personalizado con datos de muestra...')}
2025-10-17 18:14:28 +02:00
</p>
{/* Progress bar */}
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-3 mb-4 overflow-hidden">
<div
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] h-full rounded-full transition-all duration-500 ease-out"
style={{ width: `${Math.min(progressPercentage, 100)}%` }}
/>
</div>
{/* Estimated time - Only show if not complete */}
{progressPercentage < 100 && (
<div className="flex items-center justify-center text-sm text-[var(--text-tertiary)] mb-4">
<Clock className="w-4 h-4 mr-2" />
2025-11-19 07:46:40 +01:00
<span>{t('demo:loading.estimated_time', 'Tiempo estimado: ~{{seconds}}s', { seconds: estimatedTime })}</span>
2025-10-17 18:14:28 +02:00
</div>
)}
{/* Tips while loading */}
{progressPercentage < 100 && (
<div className="mt-2 p-4 bg-[var(--color-primary)]/5 rounded-lg border border-[var(--color-primary)]/20">
<p className="text-xs text-[var(--text-secondary)] italic">
2025-11-19 07:46:40 +01:00
{t('demo:loading.tip', '💡 Tip: La demo incluye datos reales de panaderías españolas para que puedas explorar todas las funcionalidades')}
2025-10-17 18:14:28 +02:00
</p>
</div>
)}
{/* Error message if any */}
{error && (
<div className="mt-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 rounded-lg text-sm">
{error}
</div>
)}
</div>
</div>
</div>
)}
2025-10-03 14:09:34 +02:00
</PublicLayout>
);
};
export default DemoPage;