Add onboarding pages

This commit is contained in:
Urtzi Alfaro
2025-09-01 08:13:01 +02:00
parent bde52d8ca2
commit 330e4a06b1
8 changed files with 518 additions and 113 deletions

View File

@@ -3,6 +3,7 @@ import { Button, Input, Card } from '../../ui';
import { useAuth } from '../../../hooks/api/useAuth';
import { UserRegistration } from '../../../types/auth.types';
import { useToast } from '../../../hooks/ui/useToast';
import { isMockRegistration } from '../../../config/mock.config';
interface RegisterFormProps {
onSuccess?: () => void;
@@ -36,7 +37,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const { register, isLoading, error } = useAuth();
const { showToast } = useToast();
const { success: showSuccessToast, error: showErrorToast } = useToast();
const validateForm = (): boolean => {
const newErrors: Partial<SimpleUserRegistration> = {};
@@ -75,6 +76,35 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
console.log('Form submitted, mock mode:', isMockRegistration());
// FORCED MOCK MODE FOR TESTING - Always bypass for now
const FORCE_MOCK = true;
if (FORCE_MOCK || isMockRegistration()) {
console.log('Mock registration triggered, showing toast and calling onSuccess');
// Show immediate success notification
try {
showSuccessToast('¡Bienvenido! Tu cuenta ha sido creada correctamente.', {
title: 'Cuenta creada exitosamente'
});
console.log('Toast shown, calling onSuccess callback');
} catch (error) {
console.error('Error showing toast:', error);
// Fallback: show browser alert if toast fails
alert('¡Cuenta creada exitosamente! Redirigiendo al onboarding...');
}
// Call success immediately (removing delay for easier testing)
try {
onSuccess?.();
console.log('onSuccess called');
} catch (error) {
console.error('Error calling onSuccess:', error);
// Fallback: direct redirect if callback fails
window.location.href = '/app/onboarding/setup';
}
return;
}
if (!validateForm()) {
return;
@@ -92,24 +122,18 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
const success = await register(registrationData);
if (success) {
showToast({
type: 'success',
title: 'Cuenta creada exitosamente',
message: '¡Bienvenido! Tu cuenta ha sido creada correctamente.'
showSuccessToast('¡Bienvenido! Tu cuenta ha sido creada correctamente.', {
title: 'Cuenta creada exitosamente'
});
onSuccess?.();
} else {
showToast({
type: 'error',
title: 'Error al crear la cuenta',
message: error || 'No se pudo crear la cuenta. Verifica que el email no esté en uso.'
showErrorToast(error || 'No se pudo crear la cuenta. Verifica que el email no esté en uso.', {
title: 'Error al crear la cuenta'
});
}
} catch (err) {
showToast({
type: 'error',
title: 'Error de conexión',
message: 'No se pudo conectar con el servidor. Verifica tu conexión a internet.'
showErrorToast('No se pudo conectar con el servidor. Verifica tu conexión a internet.', {
title: 'Error de conexión'
});
}
};
@@ -270,6 +294,10 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
loadingText="Creando cuenta..."
disabled={isLoading}
className="w-full"
onClick={(e) => {
console.log('Button clicked!');
// Let form submit handle it naturally
}}
>
Crear Cuenta
</Button>

View File

@@ -0,0 +1,37 @@
/**
* Mock configuration for testing the complete onboarding flow
* Set MOCK_MODE to false for production
*/
export const MOCK_CONFIG = {
// Global mock mode toggle
MOCK_MODE: true,
// Component-specific toggles
MOCK_REGISTRATION: true,
MOCK_AUTHENTICATION: true,
MOCK_ONBOARDING_FLOW: true,
// Mock user data
MOCK_USER: {
full_name: 'María García López',
email: 'maria.garcia@elbuenpan.com',
role: 'admin',
tenant_name: 'Panadería Artesanal El Buen Pan'
},
// Mock bakery data
MOCK_BAKERY: {
name: 'Panadería Artesanal El Buen Pan',
type: 'artisan',
location: 'Av. Principal 123, Centro Histórico',
phone: '+1 234 567 8900',
email: 'info@elbuenpan.com'
}
};
// Helper function to check if mock mode is enabled
export const isMockMode = () => MOCK_CONFIG.MOCK_MODE;
export const isMockRegistration = () => MOCK_CONFIG.MOCK_MODE && MOCK_CONFIG.MOCK_REGISTRATION;
export const isMockAuthentication = () => MOCK_CONFIG.MOCK_MODE && MOCK_CONFIG.MOCK_AUTHENTICATION;
export const isMockOnboardingFlow = () => MOCK_CONFIG.MOCK_MODE && MOCK_CONFIG.MOCK_ONBOARDING_FLOW;

View File

@@ -428,6 +428,22 @@ const OnboardingAnalysisPage: React.FC = () => {
))}
</div>
</Card>
{/* Navigation */}
<div className="flex justify-between items-center">
<Button
variant="outline"
onClick={() => window.location.href = '/app/onboarding/upload'}
>
Volver a Carga de Datos
</Button>
<Button
onClick={() => window.location.href = '/app/onboarding/review'}
>
Continuar a Revisión Final
</Button>
</div>
</div>
);
};

View File

@@ -560,7 +560,13 @@ const OnboardingReviewPage: React.FC = () => {
y el sistema está preparado para comenzar a operar.
</p>
<div className="flex space-x-3">
<Button className="bg-green-600 hover:bg-green-700">
<Button
className="bg-green-600 hover:bg-green-700"
onClick={() => {
alert('¡Felicitaciones! El onboarding ha sido completado exitosamente. Ahora puedes comenzar a usar la plataforma.');
window.location.href = '/app/dashboard';
}}
>
<Zap className="w-4 h-4 mr-2" />
Lanzar Ahora
</Button>
@@ -572,6 +578,31 @@ const OnboardingReviewPage: React.FC = () => {
</Card>
</div>
)}
{/* Overall Navigation - Always visible */}
<div className="flex justify-between items-center mt-8 p-4 bg-[var(--bg-secondary)] rounded-lg">
<Button
variant="outline"
onClick={() => window.location.href = '/app/onboarding/analysis'}
>
Volver al Análisis
</Button>
<div className="text-center">
<p className="text-sm text-[var(--text-secondary)]">Último paso del onboarding</p>
</div>
<Button
className="bg-green-600 hover:bg-green-700"
onClick={() => {
alert('¡Felicitaciones! El onboarding ha sido completado exitosamente. Ahora puedes comenzar a usar la plataforma.');
window.location.href = '/app/dashboard';
}}
>
<Zap className="w-4 h-4 mr-2" />
Finalizar Onboarding
</Button>
</div>
</div>
);
};

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { ChevronRight, ChevronLeft, Check, Store, Users, Settings, Zap } from 'lucide-react';
import { ChevronRight, ChevronLeft, Check, Store, Users, Settings, Zap, Upload } from 'lucide-react';
import { Button, Card, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
@@ -7,31 +7,38 @@ const OnboardingSetupPage: React.FC = () => {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState({
bakery: {
name: '',
type: 'traditional',
name: 'Panadería Artesanal El Buen Pan',
type: 'artisan',
size: 'medium',
location: '',
phone: '',
email: ''
location: 'Av. Principal 123, Centro Histórico',
phone: '+1 234 567 8900',
email: 'info@elbuenpan.com'
},
team: {
ownerName: '',
ownerName: 'María García López',
teamSize: '5-10',
roles: [],
experience: 'intermediate'
experience: 'experienced'
},
operations: {
openingHours: {
start: '07:00',
start: '06:00',
end: '20:00'
},
daysOpen: 6,
specialties: [],
specialties: ['bread', 'pastries', 'cakes'],
dailyProduction: 'medium'
},
upload: {
salesData: null,
inventoryData: null,
recipeData: null,
useTemplates: true,
skipUpload: false
},
goals: {
primaryGoals: [],
expectedRevenue: '',
primaryGoals: ['increase-sales', 'reduce-waste', 'improve-efficiency'],
expectedRevenue: '15000-30000',
timeline: '6months'
}
});
@@ -60,6 +67,13 @@ const OnboardingSetupPage: React.FC = () => {
},
{
id: 4,
title: 'Carga de Datos',
description: 'Importa tus datos existentes para acelerar la configuración inicial',
icon: Upload,
fields: ['files', 'templates', 'validation']
},
{
id: 5,
title: 'Objetivos',
description: 'Metas y expectativas para tu panadería',
icon: Zap,
@@ -135,7 +149,8 @@ const OnboardingSetupPage: React.FC = () => {
const handleFinish = () => {
console.log('Onboarding completed:', formData);
// Handle completion logic
// Navigate to dashboard - onboarding is complete
window.location.href = '/app/dashboard';
};
const isStepComplete = (stepId: number) => {
@@ -148,6 +163,9 @@ const OnboardingSetupPage: React.FC = () => {
case 3:
return formData.operations.specialties.length > 0;
case 4:
return formData.upload.useTemplates || formData.upload.skipUpload ||
(formData.upload.salesData || formData.upload.inventoryData || formData.upload.recipeData);
case 5:
return formData.goals.primaryGoals.length > 0;
default:
return false;
@@ -158,34 +176,76 @@ const OnboardingSetupPage: React.FC = () => {
switch (currentStep) {
case 1:
return (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
<div className="space-y-8">
<div className="space-y-3">
<label className="block text-lg font-semibold text-[var(--text-primary)] mb-3">
Nombre de la Panadería *
</label>
<Input
value={formData.bakery.name}
onChange={(e) => handleInputChange('bakery', 'name', e.target.value)}
placeholder="Ej: Panadería San Miguel"
/>
<div className="relative">
<Input
value={formData.bakery.name}
onChange={(e) => handleInputChange('bakery', 'name', e.target.value)}
placeholder="Ej: Panadería Artesanal El Buen Pan"
className="w-full transition-all duration-300 focus:scale-[1.02] focus:shadow-lg"
/>
{formData.bakery.name && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
<Check className="w-5 h-5 text-[var(--color-success)]" />
</div>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
<label className="block text-lg font-semibold text-[var(--text-primary)] mb-4">
Tipo de Panadería
</label>
<div className="grid grid-cols-2 gap-4">
{bakeryTypes.map((type) => (
<label key={type.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-[var(--bg-secondary)]">
<input
type="radio"
name="bakeryType"
value={type.value}
checked={formData.bakery.type === type.value}
onChange={(e) => handleInputChange('bakery', 'type', e.target.value)}
className="text-[var(--color-info)]"
/>
<span className="text-sm">{type.label}</span>
<label
key={type.value}
className={`
group relative flex items-center space-x-4 p-4 border-2 rounded-xl cursor-pointer transition-all duration-300 hover:scale-105 hover:shadow-lg
${formData.bakery.type === type.value
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5 shadow-lg'
: 'border-[var(--border-secondary)] bg-[var(--bg-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-tertiary)]'
}
`}
>
<div className="relative">
<input
type="radio"
name="bakeryType"
value={type.value}
checked={formData.bakery.type === type.value}
onChange={(e) => handleInputChange('bakery', 'type', e.target.value)}
className="sr-only"
/>
<div className={`
w-5 h-5 rounded-full border-2 flex items-center justify-center transition-all duration-300
${formData.bakery.type === type.value
? 'border-[var(--color-primary)] bg-[var(--color-primary)]'
: 'border-[var(--border-tertiary)] group-hover:border-[var(--color-primary)]'
}
`}>
{formData.bakery.type === type.value && (
<div className="w-2 h-2 bg-white rounded-full"></div>
)}
</div>
</div>
<span className={`
font-medium transition-colors duration-300
${formData.bakery.type === type.value ? 'text-[var(--text-primary)]' : 'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]'}
`}>
{type.label}
</span>
{/* Selection indicator */}
{formData.bakery.type === type.value && (
<div className="absolute top-2 right-2">
<Check className="w-4 h-4 text-[var(--color-primary)]" />
</div>
)}
</label>
))}
</div>
@@ -356,6 +416,136 @@ const OnboardingSetupPage: React.FC = () => {
);
case 4:
return (
<div className="space-y-8">
<div className="text-center p-6 bg-[var(--color-info)]/10 rounded-xl border border-[var(--color-info)]/20">
<Upload className="w-12 h-12 text-[var(--color-info)] mx-auto mb-4" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Acelera tu configuración
</h3>
<p className="text-[var(--text-secondary)]">
Importa tus datos existentes para configurar automáticamente productos, inventario y recetas.
</p>
</div>
{/* Upload Options */}
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Sales Data Upload */}
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-6 text-center transition-all duration-300 hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)]/5">
<div className="w-12 h-12 bg-[var(--color-success)]/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-6 h-6 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<h4 className="font-semibold text-[var(--text-primary)] mb-2">Datos de Ventas</h4>
<p className="text-sm text-[var(--text-secondary)] mb-4">
CSV con historial de ventas
</p>
<input
type="file"
accept=".csv,.xlsx,.xls"
onChange={(e) => handleInputChange('upload', 'salesData', e.target.files?.[0] || null)}
className="hidden"
id="sales-upload"
/>
<label htmlFor="sales-upload" className="btn btn-outline text-sm cursor-pointer">
{formData.upload.salesData ? 'Archivo cargado ✓' : 'Seleccionar archivo'}
</label>
</div>
{/* Inventory Data Upload */}
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-6 text-center transition-all duration-300 hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)]/5">
<div className="w-12 h-12 bg-[var(--color-warning)]/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-6 h-6 text-[var(--color-warning)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<h4 className="font-semibold text-[var(--text-primary)] mb-2">Inventario</h4>
<p className="text-sm text-[var(--text-secondary)] mb-4">
Lista de productos e ingredientes
</p>
<input
type="file"
accept=".csv,.xlsx,.xls"
onChange={(e) => handleInputChange('upload', 'inventoryData', e.target.files?.[0] || null)}
className="hidden"
id="inventory-upload"
/>
<label htmlFor="inventory-upload" className="btn btn-outline text-sm cursor-pointer">
{formData.upload.inventoryData ? 'Archivo cargado ✓' : 'Seleccionar archivo'}
</label>
</div>
{/* Recipe Data Upload */}
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-6 text-center transition-all duration-300 hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)]/5">
<div className="w-12 h-12 bg-[var(--color-info)]/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-6 h-6 text-[var(--color-info)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
</div>
<h4 className="font-semibold text-[var(--text-primary)] mb-2">Recetas</h4>
<p className="text-sm text-[var(--text-secondary)] mb-4">
Recetas y fórmulas existentes
</p>
<input
type="file"
accept=".csv,.xlsx,.xls,.pdf"
onChange={(e) => handleInputChange('upload', 'recipeData', e.target.files?.[0] || null)}
className="hidden"
id="recipe-upload"
/>
<label htmlFor="recipe-upload" className="btn btn-outline text-sm cursor-pointer">
{formData.upload.recipeData ? 'Archivo cargado ✓' : 'Seleccionar archivo'}
</label>
</div>
</div>
</div>
{/* Alternative Options */}
<div className="space-y-4 pt-6 border-t border-[var(--border-secondary)]">
<h4 className="font-semibold text-[var(--text-primary)] mb-4">Opciones alternativas</h4>
<div className="space-y-3">
<label className="flex items-center space-x-3 p-4 border rounded-lg cursor-pointer hover:bg-[var(--bg-secondary)] transition-all duration-200">
<input
type="checkbox"
checked={formData.upload.useTemplates}
onChange={(e) => handleInputChange('upload', 'useTemplates', e.target.checked)}
className="text-[var(--color-primary)] rounded"
/>
<div>
<div className="font-medium text-[var(--text-primary)]">
Usar plantillas predefinidas
</div>
<div className="text-sm text-[var(--text-secondary)]">
Comenzar con productos y recetas típicas de panadería
</div>
</div>
</label>
<label className="flex items-center space-x-3 p-4 border rounded-lg cursor-pointer hover:bg-[var(--bg-secondary)] transition-all duration-200">
<input
type="checkbox"
checked={formData.upload.skipUpload}
onChange={(e) => handleInputChange('upload', 'skipUpload', e.target.checked)}
className="text-[var(--color-primary)] rounded"
/>
<div>
<div className="font-medium text-[var(--text-primary)]">
Configurar manualmente después
</div>
<div className="text-sm text-[var(--text-secondary)]">
Saltar este paso y agregar datos posteriormente
</div>
</div>
</label>
</div>
</div>
</div>
);
case 5:
return (
<div className="space-y-6">
<div>
@@ -418,79 +608,158 @@ const OnboardingSetupPage: React.FC = () => {
};
return (
<div className="p-6 max-w-4xl mx-auto">
<PageHeader
title="Configuración Inicial"
description="Configura tu panadería paso a paso para comenzar"
/>
<div className="p-4 sm:p-6 max-w-5xl mx-auto">
<div className="text-center mb-8 sm:mb-12">
<div className="inline-flex items-center justify-center w-14 h-14 sm:w-16 sm:h-16 bg-[var(--color-primary)] rounded-full mb-4 sm:mb-6 shadow-lg">
<svg className="w-6 h-6 sm:w-8 sm:h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] mb-3 sm:mb-4">
Configuración Inicial
</h1>
<p className="text-base sm:text-lg text-[var(--text-secondary)] max-w-2xl mx-auto px-4">
Configura tu panadería paso a paso para comenzar a usar la plataforma de manera óptima
</p>
</div>
{/* Progress Steps */}
<Card className="p-6 mb-8">
<div className="flex items-center justify-between mb-6">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center">
<div className={`flex items-center justify-center w-10 h-10 rounded-full ${
step.id === currentStep
? 'bg-blue-600 text-white'
: step.id < currentStep || isStepComplete(step.id)
? 'bg-green-600 text-white'
: 'bg-[var(--bg-quaternary)] text-[var(--text-secondary)]'
}`}>
{step.id < currentStep || (step.id === currentStep && isStepComplete(step.id)) ? (
<Check className="w-5 h-5" />
) : (
<step.icon className="w-5 h-5" />
)}
</div>
{index < steps.length - 1 && (
<div className={`w-full h-1 mx-4 ${
step.id < currentStep ? 'bg-green-600' : 'bg-[var(--bg-quaternary)]'
}`} />
)}
<div className="max-w-4xl mx-auto">
{/* Enhanced Progress Section */}
<Card className="p-8 mb-8 shadow-lg">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-2">
Progreso de configuración
</h3>
<p className="text-[var(--text-secondary)]">
Completando paso {currentStep} de {steps.length}
</p>
</div>
))}
</div>
<div className="text-center">
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-2">
Paso {currentStep}: {steps[currentStep - 1].title}
</h2>
<p className="text-[var(--text-secondary)]">
{steps[currentStep - 1].description}
</p>
</div>
</Card>
<div className="text-right">
<div className="text-3xl font-bold text-[var(--color-primary)] mb-1">
{Math.round((currentStep / steps.length) * 100)}%
</div>
<p className="text-sm text-[var(--text-tertiary)]">Completado</p>
</div>
</div>
{/* Enhanced Progress Bar */}
<div className="relative mb-8">
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-3 overflow-hidden">
<div
className="bg-[var(--color-primary)] h-full rounded-full transition-all duration-700 ease-out relative"
style={{ width: `${(currentStep / steps.length) * 100}%` }}
>
<div className="absolute inset-0 bg-white opacity-20 rounded-full"></div>
</div>
</div>
</div>
{/* Step Indicators */}
<div className="flex justify-between mb-8">
{steps.map((step, index) => (
<div key={step.id} className="flex flex-col items-center group">
<div className={`
w-12 h-12 rounded-full flex items-center justify-center text-sm font-medium transition-all duration-300 transform hover:scale-105 shadow-md
${step.id <= currentStep
? 'bg-[var(--color-primary)] text-white'
: 'bg-[var(--bg-quaternary)] text-[var(--text-tertiary)] border-2 border-[var(--border-secondary)]'
}
`}>
{step.id < currentStep ? (
<Check className="w-5 h-5" />
) : step.id === currentStep ? (
<div className="w-3 h-3 bg-white rounded-full animate-pulse"></div>
) : (
<step.icon className="w-5 h-5" />
)}
</div>
{/* Connection lines */}
{index < steps.length - 1 && (
<div className={`
absolute top-6 w-16 h-1 transition-all duration-500 ml-12
${step.id < currentStep ? 'bg-[var(--color-primary)]' : 'bg-[var(--bg-quaternary)]'}
`} />
)}
<p className={`
text-xs mt-3 text-center max-w-20 leading-tight transition-all duration-300
${step.id === currentStep ? 'text-[var(--color-primary)] font-semibold' : 'text-[var(--text-tertiary)]'}
`}>
{step.title}
</p>
</div>
))}
</div>
{/* Current Step Info */}
<div className="p-6 bg-[var(--color-info)]/10 rounded-xl border border-[var(--color-info)]/20">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Paso {currentStep}: {steps[currentStep - 1].title}
</h3>
<p className="text-[var(--text-secondary)]">
{steps[currentStep - 1].description}
</p>
</div>
</Card>
{/* Step Content */}
<Card className="p-8 mb-8">
{renderStepContent()}
</Card>
{/* Enhanced Step Content */}
<Card className="p-8 mb-8 shadow-lg">
<div className="space-y-8">
{renderStepContent()}
</div>
</Card>
{/* Navigation */}
<div className="flex justify-between">
<Button
variant="outline"
onClick={prevStep}
disabled={currentStep === 1}
>
<ChevronLeft className="w-4 h-4 mr-2" />
Anterior
</Button>
{currentStep === steps.length ? (
<Button onClick={handleFinish}>
<Check className="w-4 h-4 mr-2" />
Finalizar Configuración
</Button>
) : (
{/* Enhanced Navigation */}
<div className="flex flex-col sm:flex-row justify-between items-center gap-4 sm:gap-0">
<Button
onClick={nextStep}
disabled={!isStepComplete(currentStep)}
variant="outline"
onClick={prevStep}
disabled={currentStep === 1}
className="w-full sm:w-auto flex items-center justify-center px-6 py-3 transition-all duration-300 hover:scale-105 order-2 sm:order-1"
>
Siguiente
<ChevronRight className="w-4 h-4 ml-2" />
<ChevronLeft className="w-4 h-4 mr-2" />
Anterior
</Button>
)}
<div className="flex items-center space-x-3 text-[var(--text-tertiary)] order-1 sm:order-2">
<div className="flex space-x-2">
{steps.map((_, index) => (
<div
key={index}
className={`
w-2 h-2 rounded-full transition-all duration-300
${index + 1 === currentStep ? 'bg-[var(--color-primary)] scale-150' : index + 1 < currentStep ? 'bg-[var(--color-success)]' : 'bg-[var(--bg-quaternary)]'}
`}
/>
))}
</div>
<span className="text-sm font-medium ml-4">
{currentStep} / {steps.length}
</span>
</div>
{currentStep === steps.length ? (
<Button
onClick={handleFinish}
className="w-full sm:w-auto flex items-center justify-center px-8 py-3 bg-[var(--color-success)] hover:bg-[var(--color-success)]/80 text-white transition-all duration-300 hover:scale-105 shadow-lg order-3"
>
<Check className="w-4 h-4 mr-2" />
<span className="hidden sm:inline">Finalizar Configuración</span>
<span className="sm:hidden">Finalizar</span>
</Button>
) : (
<Button
onClick={nextStep}
disabled={!isStepComplete(currentStep)}
className="w-full sm:w-auto flex items-center justify-center px-6 py-3 transition-all duration-300 hover:scale-105 order-3"
>
Siguiente
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
)}
</div>
</div>
</div>
);

View File

@@ -431,6 +431,22 @@ const OnboardingUploadPage: React.FC = () => {
</ul>
</div>
</Card>
{/* Navigation */}
<div className="flex justify-between items-center">
<Button
variant="outline"
onClick={() => window.location.href = '/app/onboarding/setup'}
>
Volver a Configuración
</Button>
<Button
onClick={() => window.location.href = '/app/onboarding/analysis'}
>
Continuar al Análisis
</Button>
</div>
</div>
);
};

View File

@@ -7,7 +7,7 @@ const RegisterPage: React.FC = () => {
const navigate = useNavigate();
const handleRegistrationSuccess = () => {
navigate('/login');
navigate('/app/onboarding/setup');
};
const handleLoginClick = () => {

View File

@@ -6,6 +6,7 @@ import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuthUser, useIsAuthenticated, useAuthLoading } from '../stores';
import { RouteConfig, canAccessRoute, ROUTES } from './routes.config';
import { isMockAuthentication } from '../config/mock.config';
interface ProtectedRouteProps {
children: React.ReactNode;
@@ -128,6 +129,13 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
const isLoading = useAuthLoading();
const location = useLocation();
// MOCK MODE - Allow access to onboarding routes for testing
const isOnboardingRoute = location.pathname.startsWith('/app/onboarding');
if (isMockAuthentication() && isOnboardingRoute) {
return <>{children}</>;
}
// Show loading spinner while checking authentication
if (isLoading) {
return fallback || <LoadingSpinner message="Verificando autenticación..." />;