Improve the demo feature of the project
This commit is contained in:
@@ -5,6 +5,7 @@ import { Button } from '../../components/ui';
|
||||
import { getDemoAccounts, createDemoSession, DemoAccount } from '../../api/services/demo';
|
||||
import { apiClient } from '../../api/client';
|
||||
import { Check, Clock, Shield, Play, Zap, ArrowRight, Store, Factory } from 'lucide-react';
|
||||
import { markTourAsStartPending } from '../../features/demo-onboarding';
|
||||
|
||||
export const DemoPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -38,9 +39,16 @@ export const DemoPage: React.FC = () => {
|
||||
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);
|
||||
|
||||
// **CRITICAL FIX: Set the virtual tenant ID in API client**
|
||||
// This ensures all API requests include the correct tenant context
|
||||
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);
|
||||
@@ -48,8 +56,34 @@ export const DemoPage: React.FC = () => {
|
||||
localStorage.setItem('demo_expires_at', session.expires_at);
|
||||
localStorage.setItem('demo_tenant_id', session.virtual_tenant_id);
|
||||
|
||||
// Navigate to dashboard
|
||||
navigate('/app/dashboard');
|
||||
// **CRITICAL FIX: Initialize tenant store with demo tenant**
|
||||
// This ensures useTenantId() returns the correct virtual tenant ID
|
||||
const { useTenantStore } = await import('../../stores/tenant.store');
|
||||
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
|
||||
markTourAsStartPending();
|
||||
|
||||
// Navigate to setup page to wait for data cloning
|
||||
navigate(`/demo/setup?session=${session.session_id}`);
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Error al crear sesión demo');
|
||||
console.error('Error creating demo session:', err);
|
||||
|
||||
236
frontend/src/pages/public/DemoSetupPage.tsx
Normal file
236
frontend/src/pages/public/DemoSetupPage.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { demoSessionAPI, SessionStatusResponse } from '@/api/services/demo';
|
||||
import { DemoProgressIndicator } from '@/components/demo/DemoProgressIndicator';
|
||||
import { DemoErrorScreen } from '@/components/demo/DemoErrorScreen';
|
||||
import { Card, CardBody, ProgressBar, Button, LoadingSpinner } from '@/components/ui';
|
||||
import { PublicLayout } from '@/components/layout';
|
||||
|
||||
const POLL_INTERVAL_MS = 1500; // Poll every 1.5 seconds
|
||||
|
||||
export const DemoSetupPage: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const sessionId = searchParams.get('session');
|
||||
|
||||
const [status, setStatus] = useState<SessionStatusResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isRetrying, setIsRetrying] = useState(false);
|
||||
|
||||
const pollStatus = useCallback(async () => {
|
||||
if (!sessionId) return;
|
||||
|
||||
try {
|
||||
const statusData = await demoSessionAPI.getSessionStatus(sessionId);
|
||||
setStatus(statusData);
|
||||
|
||||
// Redirect to dashboard if:
|
||||
// 1. Status is 'ready' (all services succeeded)
|
||||
// 2. Status is 'partial' or 'failed' BUT we have usable data (>100 records)
|
||||
const hasUsableData = statusData.total_records_cloned > 100;
|
||||
const shouldRedirect =
|
||||
statusData.status === 'ready' ||
|
||||
(statusData.status === 'partial' && hasUsableData) ||
|
||||
(statusData.status === 'failed' && hasUsableData);
|
||||
|
||||
if (shouldRedirect) {
|
||||
// Data is usable, redirect to dashboard
|
||||
setTimeout(() => {
|
||||
window.location.href = `/app/dashboard?session=${sessionId}`;
|
||||
}, 500);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||
setError(errorMessage);
|
||||
}
|
||||
}, [sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionId) {
|
||||
navigate('/demo');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial poll
|
||||
pollStatus();
|
||||
|
||||
// Set up polling interval
|
||||
const intervalId = setInterval(pollStatus, POLL_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [sessionId, navigate, pollStatus]);
|
||||
|
||||
const handleRetry = async () => {
|
||||
if (!sessionId) return;
|
||||
|
||||
try {
|
||||
setIsRetrying(true);
|
||||
setError(null);
|
||||
await demoSessionAPI.retryCloning(sessionId);
|
||||
|
||||
// Resume polling after retry
|
||||
await pollStatus();
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Retry failed';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsRetrying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContinueAnyway = () => {
|
||||
window.location.href = `/app/dashboard?session=${sessionId}`;
|
||||
};
|
||||
|
||||
if (error && !status) {
|
||||
return (
|
||||
<DemoErrorScreen
|
||||
error={error}
|
||||
onRetry={handleRetry}
|
||||
isRetrying={isRetrying}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return (
|
||||
<PublicLayout
|
||||
variant="centered"
|
||||
headerProps={{
|
||||
showThemeToggle: true,
|
||||
showAuthButtons: false,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh]">
|
||||
<LoadingSpinner size="large" />
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
Inicializando entorno demo...
|
||||
</p>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// Only show error screen if failed with NO usable data
|
||||
if (status.status === 'failed' && status.total_records_cloned <= 100) {
|
||||
return (
|
||||
<DemoErrorScreen
|
||||
error="Demo session setup failed"
|
||||
details={status.errors}
|
||||
onRetry={handleRetry}
|
||||
isRetrying={isRetrying}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const estimatedTime = estimateRemainingTime(status);
|
||||
const progressPercentage = calculateProgressPercentage(status);
|
||||
|
||||
return (
|
||||
<PublicLayout
|
||||
variant="centered"
|
||||
headerProps={{
|
||||
showThemeToggle: true,
|
||||
showAuthButtons: false,
|
||||
}}
|
||||
>
|
||||
<div className="max-w-2xl mx-auto p-8">
|
||||
<Card className="shadow-xl">
|
||||
<CardBody className="p-8">
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-3xl font-bold text-[var(--text-primary)] mb-2">
|
||||
🔄 Preparando tu Entorno Demo
|
||||
</h1>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
Configurando tu sesión personalizada con datos de muestra...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{status.progress && <DemoProgressIndicator progress={status.progress} />}
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
Progreso general
|
||||
</span>
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
{progressPercentage}%
|
||||
</span>
|
||||
</div>
|
||||
<ProgressBar value={progressPercentage} variant="primary" />
|
||||
</div>
|
||||
|
||||
{status.status === 'pending' && (
|
||||
<div className="mt-4 text-center">
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Tiempo estimado restante: ~{estimatedTime} segundos
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status.status === 'partial' && (
|
||||
<div className="mt-4 p-4 bg-[var(--color-warning)]/10 border border-[var(--color-warning)] rounded-lg">
|
||||
<p className="text-sm text-[var(--text-primary)] mb-3">
|
||||
⚠️ Algunos datos aún se están cargando. Puedes continuar con
|
||||
funcionalidad limitada o esperar a que se carguen todos los datos.
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleContinueAnyway}
|
||||
variant="warning"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
Continuar de todos modos
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status.status === 'failed' && status.total_records_cloned > 100 && (
|
||||
<div className="mt-4 p-4 bg-[var(--color-info)]/10 border border-[var(--color-info)] rounded-lg">
|
||||
<p className="text-sm text-[var(--text-primary)]">
|
||||
ℹ️ Algunos servicios tuvieron problemas, pero hemos cargado{' '}
|
||||
{status.total_records_cloned} registros exitosamente. ¡El demo está
|
||||
completamente funcional!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-xs text-[var(--text-tertiary)]">
|
||||
Total de registros clonados: {status.total_records_cloned}
|
||||
</p>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
function estimateRemainingTime(status: SessionStatusResponse): number {
|
||||
if (!status.progress) return 5;
|
||||
|
||||
const services = Object.values(status.progress);
|
||||
const completedServices = services.filter((s) => s.status === 'completed').length;
|
||||
const totalServices = services.length;
|
||||
const remainingServices = totalServices - completedServices;
|
||||
|
||||
// Assume ~2 seconds per service
|
||||
return Math.max(remainingServices * 2, 1);
|
||||
}
|
||||
|
||||
function calculateProgressPercentage(status: SessionStatusResponse): number {
|
||||
if (!status.progress) return 0;
|
||||
|
||||
const services = Object.values(status.progress);
|
||||
const completedServices = services.filter(
|
||||
(s) => s.status === 'completed' || s.status === 'failed'
|
||||
).length;
|
||||
const totalServices = services.length;
|
||||
|
||||
return Math.round((completedServices / totalServices) * 100);
|
||||
}
|
||||
|
||||
export default DemoSetupPage;
|
||||
Reference in New Issue
Block a user