Improve the demo feature of the project

This commit is contained in:
Urtzi Alfaro
2025-10-12 18:47:33 +02:00
parent dbc7f2fa0d
commit 7556a00db7
168 changed files with 10102 additions and 18869 deletions

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { PageHeader } from '../../components/layout';
@@ -10,6 +10,7 @@ import ProcurementPlansToday from '../../components/domain/dashboard/Procurement
import ProductionPlansToday from '../../components/domain/dashboard/ProductionPlansToday';
import PurchaseOrdersTracking from '../../components/domain/dashboard/PurchaseOrdersTracking';
import { useTenant } from '../../stores/tenant.store';
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
import {
AlertTriangle,
Clock,
@@ -23,6 +24,25 @@ const DashboardPage: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { availableTenants } = useTenant();
const { startTour } = useDemoTour();
const isDemoMode = localStorage.getItem('demo_mode') === 'true';
useEffect(() => {
console.log('[Dashboard] Demo mode:', isDemoMode);
console.log('[Dashboard] Should start tour:', shouldStartTour());
console.log('[Dashboard] SessionStorage demo_tour_should_start:', sessionStorage.getItem('demo_tour_should_start'));
if (isDemoMode && shouldStartTour()) {
console.log('[Dashboard] Starting tour in 1.5s...');
const timer = setTimeout(() => {
console.log('[Dashboard] Executing startTour()');
startTour();
clearTourStartPending();
}, 1500);
return () => clearTimeout(timer);
}
}, [isDemoMode, startTour]);
const handleAddNewBakery = () => {
navigate('/app/onboarding?new=true');
@@ -107,12 +127,14 @@ const DashboardPage: React.FC = () => {
/>
{/* Critical Metrics using StatsGrid */}
<StatsGrid
stats={criticalStats}
columns={4}
gap="lg"
className="mb-6"
/>
<div data-tour="dashboard-stats">
<StatsGrid
stats={criticalStats}
columns={4}
gap="lg"
className="mb-6"
/>
</div>
{/* Quick Actions - Add New Bakery */}
{availableTenants && availableTenants.length > 0 && (
@@ -153,25 +175,31 @@ const DashboardPage: React.FC = () => {
{/* Full width blocks - one after another */}
<div className="space-y-6">
{/* 1. Real-time alerts block */}
<RealTimeAlerts />
<div data-tour="real-time-alerts">
<RealTimeAlerts />
</div>
{/* 2. Purchase Orders Tracking block */}
<PurchaseOrdersTracking />
{/* 3. Procurement plans block */}
<ProcurementPlansToday
onOrderItem={handleOrderItem}
onViewDetails={handleViewDetails}
onViewAllPlans={handleViewAllPlans}
/>
<div data-tour="procurement-plans">
<ProcurementPlansToday
onOrderItem={handleOrderItem}
onViewDetails={handleViewDetails}
onViewAllPlans={handleViewAllPlans}
/>
</div>
{/* 4. Production plans block */}
<ProductionPlansToday
onStartOrder={handleStartOrder}
onPauseOrder={handlePauseOrder}
onViewDetails={handleViewDetails}
onViewAllPlans={handleViewAllPlans}
/>
<div data-tour="production-plans">
<ProductionPlansToday
onStartOrder={handleStartOrder}
onPauseOrder={handlePauseOrder}
onViewDetails={handleViewDetails}
onViewAllPlans={handleViewAllPlans}
/>
</div>
</div>
</div>
);

View File

@@ -35,6 +35,11 @@ const InventoryPage: React.FC = () => {
const tenantId = useTenantId();
// Debug tenant ID
console.log('🔍 [InventoryPage] Tenant ID from hook:', tenantId);
console.log('🔍 [InventoryPage] tenantId type:', typeof tenantId);
console.log('🔍 [InventoryPage] tenantId truthy?', !!tenantId);
// Mutations
const createIngredientMutation = useCreateIngredient();
const softDeleteMutation = useSoftDeleteIngredient();

View File

@@ -43,7 +43,39 @@ const SubscriptionPage: React.FC = () => {
subscriptionService.getAvailablePlans()
]);
setUsageSummary(usage);
// FIX: Handle demo mode or missing subscription data
if (!usage || !usage.usage) {
// If no usage data, likely a demo tenant - create mock data
const mockUsage: UsageSummary = {
plan: 'demo',
status: 'active',
monthly_price: 0,
next_billing_date: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
usage: {
users: {
current: 1,
limit: 5,
unlimited: false,
usage_percentage: 20
},
locations: {
current: 1,
limit: 1,
unlimited: false,
usage_percentage: 100
},
products: {
current: 0,
limit: 50,
unlimited: false,
usage_percentage: 0
}
}
};
setUsageSummary(mockUsage);
} else {
setUsageSummary(usage);
}
setAvailablePlans(plans);
} catch (error) {
console.error('Error loading subscription data:', error);

View File

@@ -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);

View 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;