407 lines
17 KiB
TypeScript
407 lines
17 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { PublicLayout } from '../../components/layout';
|
|
import { Button } from '../../components/ui';
|
|
import { getDemoAccounts, createDemoSession, DemoAccount, demoSessionAPI } from '../../api/services/demo';
|
|
import { apiClient } from '../../api/client';
|
|
import { Check, Clock, Shield, Play, Zap, ArrowRight, Store, Factory, Loader2 } from 'lucide-react';
|
|
import { markTourAsStartPending } from '../../features/demo-onboarding';
|
|
|
|
const POLL_INTERVAL_MS = 1500; // Poll every 1.5 seconds
|
|
|
|
export const DemoPage: React.FC = () => {
|
|
const [demoAccounts, setDemoAccounts] = useState<DemoAccount[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [creatingSession, setCreatingSession] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [progressPercentage, setProgressPercentage] = useState(0);
|
|
const [estimatedTime, setEstimatedTime] = useState(5);
|
|
|
|
useEffect(() => {
|
|
const fetchDemoAccounts = async () => {
|
|
try {
|
|
const accounts = await getDemoAccounts();
|
|
setDemoAccounts(accounts);
|
|
} catch (err) {
|
|
setError('Error al cargar las cuentas demo');
|
|
console.error('Error fetching demo accounts:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchDemoAccounts();
|
|
}, []);
|
|
|
|
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
|
|
const hasUsableData = statusData.total_records_cloned > 100;
|
|
const shouldRedirect =
|
|
statusData.status === 'ready' ||
|
|
(statusData.status === 'partial' && hasUsableData) ||
|
|
(statusData.status === 'failed' && hasUsableData);
|
|
|
|
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;
|
|
}
|
|
}, []);
|
|
|
|
const handleStartDemo = async (accountType: string) => {
|
|
setCreatingSession(true);
|
|
setError(null);
|
|
setProgressPercentage(0);
|
|
setEstimatedTime(6);
|
|
|
|
try {
|
|
const session = await createDemoSession({
|
|
demo_account_type: accountType as 'individual_bakery' | 'central_baker',
|
|
});
|
|
|
|
console.log('✅ Demo session created:', session);
|
|
|
|
// Store session ID in API client
|
|
apiClient.setDemoSessionId(session.session_id);
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
}
|
|
} catch (err: any) {
|
|
setError(err?.message || 'Error al crear sesión demo');
|
|
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>
|
|
<p className="mt-4 text-[var(--text-secondary)]">Cargando cuentas demo...</p>
|
|
</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" />
|
|
Demo Interactiva
|
|
</span>
|
|
</div>
|
|
|
|
<h1 className="text-4xl tracking-tight font-extrabold text-[var(--text-primary)] sm:text-5xl lg:text-6xl">
|
|
<span className="block">Prueba BakeryIA</span>
|
|
<span className="block text-[var(--color-primary)]">sin compromiso</span>
|
|
</h1>
|
|
|
|
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)] sm:text-xl">
|
|
Explora nuestro sistema con datos reales de panaderías españolas.
|
|
Elige el tipo de negocio que mejor se adapte a tu caso.
|
|
</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" />
|
|
Sin tarjeta de crédito
|
|
</div>
|
|
<div className="flex items-center">
|
|
<Clock className="w-4 h-4 text-green-500 mr-2" />
|
|
30 minutos de acceso
|
|
</div>
|
|
<div className="flex items-center">
|
|
<Shield className="w-4 h-4 text-green-500 mr-2" />
|
|
Datos aislados y seguros
|
|
</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)]">
|
|
{account.name}
|
|
</h2>
|
|
<p className="text-sm text-[var(--text-tertiary)] mt-1">
|
|
{account.business_model}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<span className="px-3 py-1 bg-[var(--color-primary)]/10 text-[var(--color-primary)] rounded-full text-xs font-semibold">
|
|
DEMO
|
|
</span>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<p className="text-[var(--text-secondary)] mb-6">
|
|
{account.description}
|
|
</p>
|
|
|
|
{/* Features */}
|
|
{account.features && account.features.length > 0 && (
|
|
<div className="mb-6 space-y-2">
|
|
<p className="text-sm font-semibold text-[var(--text-primary)] mb-3">
|
|
Funcionalidades incluidas:
|
|
</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>
|
|
)}
|
|
|
|
{/* Demo Benefits */}
|
|
<div className="space-y-2 mb-8 pt-6 border-t border-[var(--border-default)]">
|
|
<div className="flex items-center text-sm text-[var(--text-secondary)]">
|
|
<Check className="w-4 h-4 mr-2 text-green-500" />
|
|
Datos reales en español
|
|
</div>
|
|
<div className="flex items-center text-sm text-[var(--text-secondary)]">
|
|
<Check className="w-4 h-4 mr-2 text-green-500" />
|
|
Sesión aislada de 30 minutos
|
|
</div>
|
|
<div className="flex items-center text-sm text-[var(--text-secondary)]">
|
|
<Check className="w-4 h-4 mr-2 text-green-500" />
|
|
Sin necesidad de registro
|
|
</div>
|
|
</div>
|
|
|
|
{/* CTA Button */}
|
|
<Button
|
|
onClick={() => handleStartDemo(account.account_type)}
|
|
disabled={creatingSession}
|
|
size="lg"
|
|
className="w-full bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200"
|
|
>
|
|
<Play className="mr-2 w-5 h-5" />
|
|
Probar Demo Ahora
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Footer CTA */}
|
|
<div className="mt-16 text-center">
|
|
<p className="text-[var(--text-secondary)] mb-4">
|
|
¿Ya tienes una cuenta?
|
|
</p>
|
|
<Link
|
|
to="/login"
|
|
className="inline-flex items-center text-[var(--color-primary)] hover:text-[var(--color-primary-dark)] font-semibold transition-colors"
|
|
>
|
|
Inicia sesión aquí
|
|
<ArrowRight className="ml-2 w-4 h-4" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 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">
|
|
{progressPercentage >= 100 ? '¡Listo! Redirigiendo...' : 'Preparando tu Demo'}
|
|
</h2>
|
|
<p className="text-[var(--text-secondary)] mb-6">
|
|
{progressPercentage >= 100
|
|
? 'Tu entorno está listo. Accediendo al dashboard...'
|
|
: 'Configurando tu entorno personalizado con datos de muestra...'}
|
|
</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" />
|
|
<span>Tiempo estimado: ~{estimatedTime}s</span>
|
|
</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">
|
|
💡 Tip: La demo incluye datos reales de panaderías españolas para que puedas explorar todas las funcionalidades
|
|
</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>
|
|
)}
|
|
</PublicLayout>
|
|
);
|
|
};
|
|
|
|
export default DemoPage;
|