Add onboarding flow improvements

This commit is contained in:
Urtzi Alfaro
2025-09-03 14:06:38 +02:00
parent 0fb9f9d0f0
commit a55d48e635
31 changed files with 3813 additions and 6251 deletions

View File

@@ -0,0 +1,161 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { OnboardingWizard, OnboardingStep } from '../../../components/domain/onboarding/OnboardingWizard';
import { onboardingApiService } from '../../../services/api/onboarding.service';
import { useAuth } from '../../../hooks/useAuth';
import { LoadingSpinner } from '../../../components/shared/LoadingSpinner';
// Step Components
import { BakerySetupStep } from '../../../components/domain/onboarding/steps/BakerySetupStep';
import { DataProcessingStep } from '../../../components/domain/onboarding/steps/DataProcessingStep';
import { ReviewStep } from '../../../components/domain/onboarding/steps/ReviewStep';
import { InventorySetupStep } from '../../../components/domain/onboarding/steps/InventorySetupStep';
import { SuppliersStep } from '../../../components/domain/onboarding/steps/SuppliersStep';
import { MLTrainingStep } from '../../../components/domain/onboarding/steps/MLTrainingStep';
import { CompletionStep } from '../../../components/domain/onboarding/steps/CompletionStep';
const OnboardingPage: React.FC = () => {
const navigate = useNavigate();
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [globalData, setGlobalData] = useState<any>({});
// Define the 8 onboarding steps (simplified by merging data upload + analysis)
const steps: OnboardingStep[] = [
{
id: 'setup',
title: '🏢 Setup',
description: 'Configuración básica de tu panadería y creación del tenant',
component: BakerySetupStep,
isRequired: true,
validation: (data) => {
if (!data.bakery?.name) return 'El nombre de la panadería es requerido';
if (!data.bakery?.type) return 'El tipo de panadería es requerido';
if (!data.bakery?.location) return 'La ubicación es requerida';
// Tenant creation will happen automatically when validation passes
return null;
}
},
{
id: 'data-processing',
title: '📊 Historial de Ventas',
description: 'Sube tus datos de ventas para obtener insights personalizados',
component: DataProcessingStep,
isRequired: true,
validation: (data) => {
if (!data.files?.salesData) return 'Debes cargar el archivo de datos de ventas';
if (data.processingStage !== 'completed') return 'El procesamiento debe completarse antes de continuar';
if (!data.processingResults?.is_valid) return 'Los datos deben ser válidos para continuar';
return null;
}
},
{
id: 'review',
title: '📋 Revisión',
description: 'Revisión de productos detectados por IA y resultados',
component: ReviewStep,
isRequired: true,
validation: (data) => {
if (!data.reviewCompleted) return 'Debes revisar y aprobar los productos detectados';
return null;
}
},
{
id: 'inventory',
title: '⚙️ Inventario',
description: 'Configuración de inventario (stock, fechas de vencimiento)',
component: InventorySetupStep,
isRequired: true,
validation: (data) => {
if (!data.inventoryConfigured) return 'Debes configurar el inventario básico';
return null;
}
},
{
id: 'suppliers',
title: '🏪 Proveedores',
description: 'Configuración de proveedores y asociaciones',
component: SuppliersStep,
isRequired: false,
validation: () => null // Optional step
},
{
id: 'ml-training',
title: '🎯 Inteligencia',
description: 'Creación de tu asistente inteligente personalizado',
component: MLTrainingStep,
isRequired: true,
validation: (data) => {
if (data.trainingStatus !== 'completed') return 'El entrenamiento del modelo debe completarse';
return null;
}
},
{
id: 'completion',
title: '🎉 Listo',
description: 'Finalización y preparación para usar la plataforma',
component: CompletionStep,
isRequired: true,
validation: () => null
}
];
const handleComplete = async (allData: any) => {
setIsLoading(true);
try {
// Mark onboarding as complete in the backend
if (user?.tenant_id) {
await onboardingApiService.completeOnboarding(user.tenant_id, {
completedAt: new Date().toISOString(),
data: allData
});
}
// Navigate to dashboard
navigate('/app/dashboard', {
state: {
message: '¡Felicidades! Tu panadería ha sido configurada exitosamente.',
type: 'success'
}
});
} catch (error) {
console.error('Error completing onboarding:', error);
// Still navigate to dashboard but show warning
navigate('/app/dashboard', {
state: {
message: 'Configuración completada. Algunos ajustes finales pueden estar pendientes.',
type: 'warning'
}
});
} finally {
setIsLoading(false);
}
};
const handleExit = () => {
const confirmExit = window.confirm(
'¿Estás seguro de que quieres salir del proceso de configuración? Tu progreso se guardará automáticamente.'
);
if (confirmExit) {
navigate('/app/dashboard');
}
};
if (isLoading) {
return <LoadingSpinner overlay text="Completando configuración..." />;
}
return (
<div className="min-h-screen bg-[var(--bg-primary)]">
<OnboardingWizard
steps={steps}
onComplete={handleComplete}
onExit={handleExit}
className="py-8"
/>
</div>
);
};
export default OnboardingPage;

View File

@@ -1,451 +0,0 @@
import React, { useState } from 'react';
import { BarChart3, TrendingUp, Target, AlertCircle, CheckCircle, Eye, Download } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingAnalysisPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('30days');
const analysisData = {
onboardingScore: 87,
completionRate: 92,
averageTime: '4.2 días',
stepsCompleted: 15,
totalSteps: 16,
dataQuality: 94
};
const stepProgress = [
{ step: 'Información Básica', completed: true, quality: 95, timeSpent: '25 min' },
{ step: 'Configuración de Menú', completed: true, quality: 88, timeSpent: '1.2 horas' },
{ step: 'Datos de Inventario', completed: true, quality: 92, timeSpent: '45 min' },
{ step: 'Configuración de Horarios', completed: true, quality: 100, timeSpent: '15 min' },
{ step: 'Integración de Pagos', completed: true, quality: 85, timeSpent: '30 min' },
{ step: 'Carga de Productos', completed: true, quality: 90, timeSpent: '2.1 horas' },
{ step: 'Configuración de Personal', completed: true, quality: 87, timeSpent: '40 min' },
{ step: 'Pruebas del Sistema', completed: false, quality: 0, timeSpent: '-' }
];
const insights = [
{
type: 'success',
title: 'Excelente Progreso',
description: 'Has completado el 94% del proceso de configuración inicial',
recommendation: 'Solo faltan las pruebas finales del sistema para completar tu configuración',
impact: 'high'
},
{
type: 'info',
title: 'Calidad de Datos Alta',
description: 'Tus datos tienen una calidad promedio del 94%',
recommendation: 'Considera revisar la configuración de pagos para mejorar la puntuación',
impact: 'medium'
},
{
type: 'warning',
title: 'Paso Pendiente',
description: 'Las pruebas del sistema están pendientes',
recommendation: 'Programa las pruebas para validar la configuración completa',
impact: 'high'
}
];
const dataAnalysis = [
{
category: 'Información del Negocio',
completeness: 100,
accuracy: 95,
items: 12,
issues: 0,
details: 'Toda la información básica está completa y verificada'
},
{
category: 'Menú y Productos',
completeness: 85,
accuracy: 88,
items: 45,
issues: 3,
details: '3 productos sin precios definidos'
},
{
category: 'Inventario Inicial',
completeness: 92,
accuracy: 90,
items: 28,
issues: 2,
details: '2 ingredientes sin stock mínimo definido'
},
{
category: 'Configuración Operativa',
completeness: 100,
accuracy: 100,
items: 8,
issues: 0,
details: 'Horarios y políticas completamente configuradas'
}
];
const benchmarkComparison = {
industry: {
onboardingScore: 74,
completionRate: 78,
averageTime: '6.8 días'
},
yourData: {
onboardingScore: 87,
completionRate: 92,
averageTime: '4.2 días'
}
};
const recommendations = [
{
priority: 'high',
title: 'Completar Pruebas del Sistema',
description: 'Realizar pruebas integrales para validar toda la configuración',
estimatedTime: '30 minutos',
impact: 'Garantiza funcionamiento óptimo del sistema'
},
{
priority: 'medium',
title: 'Revisar Precios de Productos',
description: 'Definir precios para los 3 productos pendientes',
estimatedTime: '15 minutos',
impact: 'Permitirá generar ventas de todos los productos'
},
{
priority: 'medium',
title: 'Configurar Stocks Mínimos',
description: 'Establecer niveles mínimos para 2 ingredientes',
estimatedTime: '10 minutos',
impact: 'Mejorará el control de inventario automático'
},
{
priority: 'low',
title: 'Optimizar Configuración de Pagos',
description: 'Revisar métodos de pago y comisiones',
estimatedTime: '20 minutos',
impact: 'Puede reducir costos de transacción'
}
];
const getInsightIcon = (type: string) => {
const iconProps = { className: "w-5 h-5" };
switch (type) {
case 'success': return <CheckCircle {...iconProps} className="w-5 h-5 text-[var(--color-success)]" />;
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
case 'info': return <Target {...iconProps} className="w-5 h-5 text-[var(--color-info)]" />;
default: return <AlertCircle {...iconProps} />;
}
};
const getInsightColor = (type: string) => {
switch (type) {
case 'success': return 'bg-green-50 border-green-200';
case 'warning': return 'bg-yellow-50 border-yellow-200';
case 'info': return 'bg-[var(--color-info)]/5 border-[var(--color-info)]/20';
default: return 'bg-[var(--bg-secondary)] border-[var(--border-primary)]';
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getCompletionColor = (percentage: number) => {
if (percentage >= 95) return 'text-[var(--color-success)]';
if (percentage >= 80) return 'text-yellow-600';
return 'text-[var(--color-error)]';
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Configuración"
description="Análisis detallado de tu proceso de configuración y recomendaciones"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar Reporte
</Button>
</div>
}
/>
{/* Overall Score */}
<Card className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-6">
<div className="text-center">
<div className="relative w-24 h-24 mx-auto mb-3">
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-2xl font-bold text-[var(--color-success)]">{analysisData.onboardingScore}</span>
</div>
<svg className="w-24 h-24 transform -rotate-90">
<circle
cx="48"
cy="48"
r="40"
stroke="currentColor"
strokeWidth="8"
fill="none"
className="text-gray-200"
/>
<circle
cx="48"
cy="48"
r="40"
stroke="currentColor"
strokeWidth="8"
fill="none"
strokeDasharray={`${analysisData.onboardingScore * 2.51} 251`}
className="text-[var(--color-success)]"
/>
</svg>
</div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Puntuación General</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-[var(--color-info)]">{analysisData.completionRate}%</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Completado</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-purple-600">{analysisData.averageTime}</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Tiempo Promedio</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-[var(--color-primary)]">{analysisData.stepsCompleted}/{analysisData.totalSteps}</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Pasos Completados</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-teal-600">{analysisData.dataQuality}%</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Calidad de Datos</p>
</div>
<div className="text-center">
<div className="flex items-center justify-center mb-2">
<TrendingUp className="w-8 h-8 text-[var(--color-success)]" />
</div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Por encima del promedio</p>
</div>
</div>
</Card>
{/* Progress Analysis */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Progreso por Pasos</h3>
<div className="space-y-4">
{stepProgress.map((step, index) => (
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center space-x-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
step.completed ? 'bg-[var(--color-success)]/10' : 'bg-[var(--bg-tertiary)]'
}`}>
{step.completed ? (
<CheckCircle className="w-5 h-5 text-[var(--color-success)]" />
) : (
<span className="text-sm font-medium text-[var(--text-tertiary)]">{index + 1}</span>
)}
</div>
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">{step.step}</p>
<p className="text-xs text-[var(--text-tertiary)]">Tiempo: {step.timeSpent}</p>
</div>
</div>
<div className="text-right">
<p className={`text-sm font-medium ${getCompletionColor(step.quality)}`}>
{step.quality}%
</p>
<p className="text-xs text-[var(--text-tertiary)]">Calidad</p>
</div>
</div>
))}
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Comparación con la Industria</h3>
<div className="space-y-6">
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-[var(--text-secondary)]">Puntuación de Configuración</span>
<div className="text-right">
<span className="text-lg font-bold text-[var(--color-success)]">{benchmarkComparison.yourData.onboardingScore}</span>
<span className="text-sm text-[var(--text-tertiary)] ml-2">vs {benchmarkComparison.industry.onboardingScore}</span>
</div>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div className="bg-green-600 h-2 rounded-full" style={{ width: `${(benchmarkComparison.yourData.onboardingScore / 100) * 100}%` }}></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-[var(--text-secondary)]">Tasa de Completado</span>
<div className="text-right">
<span className="text-lg font-bold text-[var(--color-info)]">{benchmarkComparison.yourData.completionRate}%</span>
<span className="text-sm text-[var(--text-tertiary)] ml-2">vs {benchmarkComparison.industry.completionRate}%</span>
</div>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: `${benchmarkComparison.yourData.completionRate}%` }}></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-[var(--text-secondary)]">Tiempo de Configuración</span>
<div className="text-right">
<span className="text-lg font-bold text-purple-600">{benchmarkComparison.yourData.averageTime}</span>
<span className="text-sm text-[var(--text-tertiary)] ml-2">vs {benchmarkComparison.industry.averageTime}</span>
</div>
</div>
<div className="bg-green-50 p-3 rounded-lg">
<p className="text-sm text-[var(--color-success)]">38% más rápido que el promedio de la industria</p>
</div>
</div>
</div>
</Card>
</div>
{/* Insights */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Insights y Recomendaciones</h3>
<div className="space-y-4">
{insights.map((insight, index) => (
<div key={index} className={`p-4 rounded-lg border ${getInsightColor(insight.type)}`}>
<div className="flex items-start space-x-3">
{getInsightIcon(insight.type)}
<div className="flex-1">
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-1">{insight.title}</h4>
<p className="text-sm text-[var(--text-secondary)] mb-2">{insight.description}</p>
<p className="text-sm font-medium text-[var(--text-primary)]">
Recomendación: {insight.recommendation}
</p>
</div>
<Badge variant={insight.impact === 'high' ? 'red' : insight.impact === 'medium' ? 'yellow' : 'green'}>
{insight.impact === 'high' ? 'Alto' : insight.impact === 'medium' ? 'Medio' : 'Bajo'} Impacto
</Badge>
</div>
</div>
))}
</div>
</Card>
{/* Data Analysis */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Análisis de Calidad de Datos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{dataAnalysis.map((category, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-[var(--text-primary)]">{category.category}</h4>
<div className="flex items-center space-x-2">
<BarChart3 className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-sm text-[var(--text-tertiary)]">{category.items} elementos</span>
</div>
</div>
<div className="space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span>Completitud</span>
<span className={getCompletionColor(category.completeness)}>{category.completeness}%</span>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${category.completeness}%` }}
></div>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span>Precisión</span>
<span className={getCompletionColor(category.accuracy)}>{category.accuracy}%</span>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div
className="bg-green-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${category.accuracy}%` }}
></div>
</div>
</div>
{category.issues > 0 && (
<div className="flex items-center text-sm text-[var(--color-error)]">
<AlertCircle className="w-4 h-4 mr-1" />
<span>{category.issues} problema{category.issues > 1 ? 's' : ''} detectado{category.issues > 1 ? 's' : ''}</span>
</div>
)}
<p className="text-xs text-[var(--text-secondary)]">{category.details}</p>
</div>
</div>
))}
</div>
</Card>
{/* Action Items */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Elementos de Acción</h3>
<div className="space-y-3">
{recommendations.map((rec, index) => (
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-start space-x-3 flex-1">
<Badge variant={getPriorityColor(rec.priority)}>
{rec.priority === 'high' ? 'Alta' : rec.priority === 'medium' ? 'Media' : 'Baja'}
</Badge>
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-1">{rec.title}</h4>
<p className="text-sm text-[var(--text-secondary)] mb-1">{rec.description}</p>
<div className="flex items-center space-x-4 text-xs text-[var(--text-tertiary)]">
<span>Tiempo estimado: {rec.estimatedTime}</span>
<span></span>
<span>Impacto: {rec.impact}</span>
</div>
</div>
</div>
<Button size="sm">
Completar
</Button>
</div>
))}
</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>
);
};
export default OnboardingAnalysisPage;

View File

@@ -1,435 +0,0 @@
import React, { useState } from 'react';
import { BarChart3, TrendingUp, Target, AlertCircle, CheckCircle, Eye, Download } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingAnalysisPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('30days');
const analysisData = {
onboardingScore: 87,
completionRate: 92,
averageTime: '4.2 días',
stepsCompleted: 15,
totalSteps: 16,
dataQuality: 94
};
const stepProgress = [
{ step: 'Información Básica', completed: true, quality: 95, timeSpent: '25 min' },
{ step: 'Configuración de Menú', completed: true, quality: 88, timeSpent: '1.2 horas' },
{ step: 'Datos de Inventario', completed: true, quality: 92, timeSpent: '45 min' },
{ step: 'Configuración de Horarios', completed: true, quality: 100, timeSpent: '15 min' },
{ step: 'Integración de Pagos', completed: true, quality: 85, timeSpent: '30 min' },
{ step: 'Carga de Productos', completed: true, quality: 90, timeSpent: '2.1 horas' },
{ step: 'Configuración de Personal', completed: true, quality: 87, timeSpent: '40 min' },
{ step: 'Pruebas del Sistema', completed: false, quality: 0, timeSpent: '-' }
];
const insights = [
{
type: 'success',
title: 'Excelente Progreso',
description: 'Has completado el 94% del proceso de configuración inicial',
recommendation: 'Solo faltan las pruebas finales del sistema para completar tu configuración',
impact: 'high'
},
{
type: 'info',
title: 'Calidad de Datos Alta',
description: 'Tus datos tienen una calidad promedio del 94%',
recommendation: 'Considera revisar la configuración de pagos para mejorar la puntuación',
impact: 'medium'
},
{
type: 'warning',
title: 'Paso Pendiente',
description: 'Las pruebas del sistema están pendientes',
recommendation: 'Programa las pruebas para validar la configuración completa',
impact: 'high'
}
];
const dataAnalysis = [
{
category: 'Información del Negocio',
completeness: 100,
accuracy: 95,
items: 12,
issues: 0,
details: 'Toda la información básica está completa y verificada'
},
{
category: 'Menú y Productos',
completeness: 85,
accuracy: 88,
items: 45,
issues: 3,
details: '3 productos sin precios definidos'
},
{
category: 'Inventario Inicial',
completeness: 92,
accuracy: 90,
items: 28,
issues: 2,
details: '2 ingredientes sin stock mínimo definido'
},
{
category: 'Configuración Operativa',
completeness: 100,
accuracy: 100,
items: 8,
issues: 0,
details: 'Horarios y políticas completamente configuradas'
}
];
const benchmarkComparison = {
industry: {
onboardingScore: 74,
completionRate: 78,
averageTime: '6.8 días'
},
yourData: {
onboardingScore: 87,
completionRate: 92,
averageTime: '4.2 días'
}
};
const recommendations = [
{
priority: 'high',
title: 'Completar Pruebas del Sistema',
description: 'Realizar pruebas integrales para validar toda la configuración',
estimatedTime: '30 minutos',
impact: 'Garantiza funcionamiento óptimo del sistema'
},
{
priority: 'medium',
title: 'Revisar Precios de Productos',
description: 'Definir precios para los 3 productos pendientes',
estimatedTime: '15 minutos',
impact: 'Permitirá generar ventas de todos los productos'
},
{
priority: 'medium',
title: 'Configurar Stocks Mínimos',
description: 'Establecer niveles mínimos para 2 ingredientes',
estimatedTime: '10 minutos',
impact: 'Mejorará el control de inventario automático'
},
{
priority: 'low',
title: 'Optimizar Configuración de Pagos',
description: 'Revisar métodos de pago y comisiones',
estimatedTime: '20 minutos',
impact: 'Puede reducir costos de transacción'
}
];
const getInsightIcon = (type: string) => {
const iconProps = { className: "w-5 h-5" };
switch (type) {
case 'success': return <CheckCircle {...iconProps} className="w-5 h-5 text-green-600" />;
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
case 'info': return <Target {...iconProps} className="w-5 h-5 text-blue-600" />;
default: return <AlertCircle {...iconProps} />;
}
};
const getInsightColor = (type: string) => {
switch (type) {
case 'success': return 'bg-green-50 border-green-200';
case 'warning': return 'bg-yellow-50 border-yellow-200';
case 'info': return 'bg-blue-50 border-blue-200';
default: return 'bg-gray-50 border-gray-200';
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getCompletionColor = (percentage: number) => {
if (percentage >= 95) return 'text-green-600';
if (percentage >= 80) return 'text-yellow-600';
return 'text-red-600';
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Configuración"
description="Análisis detallado de tu proceso de configuración y recomendaciones"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar Reporte
</Button>
</div>
}
/>
{/* Overall Score */}
<Card className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-6">
<div className="text-center">
<div className="relative w-24 h-24 mx-auto mb-3">
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-2xl font-bold text-green-600">{analysisData.onboardingScore}</span>
</div>
<svg className="w-24 h-24 transform -rotate-90">
<circle
cx="48"
cy="48"
r="40"
stroke="currentColor"
strokeWidth="8"
fill="none"
className="text-gray-200"
/>
<circle
cx="48"
cy="48"
r="40"
stroke="currentColor"
strokeWidth="8"
fill="none"
strokeDasharray={`${analysisData.onboardingScore * 2.51} 251`}
className="text-green-600"
/>
</svg>
</div>
<p className="text-sm font-medium text-gray-700">Puntuación General</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-blue-600">{analysisData.completionRate}%</p>
<p className="text-sm font-medium text-gray-700">Completado</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-purple-600">{analysisData.averageTime}</p>
<p className="text-sm font-medium text-gray-700">Tiempo Promedio</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-orange-600">{analysisData.stepsCompleted}/{analysisData.totalSteps}</p>
<p className="text-sm font-medium text-gray-700">Pasos Completados</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-teal-600">{analysisData.dataQuality}%</p>
<p className="text-sm font-medium text-gray-700">Calidad de Datos</p>
</div>
<div className="text-center">
<div className="flex items-center justify-center mb-2">
<TrendingUp className="w-8 h-8 text-green-600" />
</div>
<p className="text-sm font-medium text-gray-700">Por encima del promedio</p>
</div>
</div>
</Card>
{/* Progress Analysis */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Progreso por Pasos</h3>
<div className="space-y-4">
{stepProgress.map((step, index) => (
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center space-x-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
step.completed ? 'bg-green-100' : 'bg-gray-100'
}`}>
{step.completed ? (
<CheckCircle className="w-5 h-5 text-green-600" />
) : (
<span className="text-sm font-medium text-gray-500">{index + 1}</span>
)}
</div>
<div>
<p className="text-sm font-medium text-gray-900">{step.step}</p>
<p className="text-xs text-gray-500">Tiempo: {step.timeSpent}</p>
</div>
</div>
<div className="text-right">
<p className={`text-sm font-medium ${getCompletionColor(step.quality)}`}>
{step.quality}%
</p>
<p className="text-xs text-gray-500">Calidad</p>
</div>
</div>
))}
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Comparación con la Industria</h3>
<div className="space-y-6">
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700">Puntuación de Configuración</span>
<div className="text-right">
<span className="text-lg font-bold text-green-600">{benchmarkComparison.yourData.onboardingScore}</span>
<span className="text-sm text-gray-500 ml-2">vs {benchmarkComparison.industry.onboardingScore}</span>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-green-600 h-2 rounded-full" style={{ width: `${(benchmarkComparison.yourData.onboardingScore / 100) * 100}%` }}></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700">Tasa de Completado</span>
<div className="text-right">
<span className="text-lg font-bold text-blue-600">{benchmarkComparison.yourData.completionRate}%</span>
<span className="text-sm text-gray-500 ml-2">vs {benchmarkComparison.industry.completionRate}%</span>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: `${benchmarkComparison.yourData.completionRate}%` }}></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700">Tiempo de Configuración</span>
<div className="text-right">
<span className="text-lg font-bold text-purple-600">{benchmarkComparison.yourData.averageTime}</span>
<span className="text-sm text-gray-500 ml-2">vs {benchmarkComparison.industry.averageTime}</span>
</div>
</div>
<div className="bg-green-50 p-3 rounded-lg">
<p className="text-sm text-green-700">38% más rápido que el promedio de la industria</p>
</div>
</div>
</div>
</Card>
</div>
{/* Insights */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Insights y Recomendaciones</h3>
<div className="space-y-4">
{insights.map((insight, index) => (
<div key={index} className={`p-4 rounded-lg border ${getInsightColor(insight.type)}`}>
<div className="flex items-start space-x-3">
{getInsightIcon(insight.type)}
<div className="flex-1">
<h4 className="text-sm font-medium text-gray-900 mb-1">{insight.title}</h4>
<p className="text-sm text-gray-700 mb-2">{insight.description}</p>
<p className="text-sm font-medium text-gray-900">
Recomendación: {insight.recommendation}
</p>
</div>
<Badge variant={insight.impact === 'high' ? 'red' : insight.impact === 'medium' ? 'yellow' : 'green'}>
{insight.impact === 'high' ? 'Alto' : insight.impact === 'medium' ? 'Medio' : 'Bajo'} Impacto
</Badge>
</div>
</div>
))}
</div>
</Card>
{/* Data Analysis */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Análisis de Calidad de Datos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{dataAnalysis.map((category, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-gray-900">{category.category}</h4>
<div className="flex items-center space-x-2">
<BarChart3 className="w-4 h-4 text-gray-500" />
<span className="text-sm text-gray-500">{category.items} elementos</span>
</div>
</div>
<div className="space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span>Completitud</span>
<span className={getCompletionColor(category.completeness)}>{category.completeness}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${category.completeness}%` }}
></div>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span>Precisión</span>
<span className={getCompletionColor(category.accuracy)}>{category.accuracy}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-green-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${category.accuracy}%` }}
></div>
</div>
</div>
{category.issues > 0 && (
<div className="flex items-center text-sm text-red-600">
<AlertCircle className="w-4 h-4 mr-1" />
<span>{category.issues} problema{category.issues > 1 ? 's' : ''} detectado{category.issues > 1 ? 's' : ''}</span>
</div>
)}
<p className="text-xs text-gray-600">{category.details}</p>
</div>
</div>
))}
</div>
</Card>
{/* Action Items */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Elementos de Acción</h3>
<div className="space-y-3">
{recommendations.map((rec, index) => (
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-start space-x-3 flex-1">
<Badge variant={getPriorityColor(rec.priority)}>
{rec.priority === 'high' ? 'Alta' : rec.priority === 'medium' ? 'Media' : 'Baja'}
</Badge>
<div>
<h4 className="text-sm font-medium text-gray-900 mb-1">{rec.title}</h4>
<p className="text-sm text-gray-600 mb-1">{rec.description}</p>
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span>Tiempo estimado: {rec.estimatedTime}</span>
<span>•</span>
<span>Impacto: {rec.impact}</span>
</div>
</div>
</div>
<Button size="sm">
Completar
</Button>
</div>
))}
</div>
</Card>
</div>
);
};
export default OnboardingAnalysisPage;

View File

@@ -1 +0,0 @@
export { default as OnboardingAnalysisPage } from './OnboardingAnalysisPage';

View File

@@ -1,610 +0,0 @@
import React, { useState } from 'react';
import { CheckCircle, AlertCircle, Edit2, Eye, Star, ArrowRight, Clock, Users, Zap } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingReviewPage: React.FC = () => {
const [activeSection, setActiveSection] = useState<string>('overview');
const completionData = {
overallProgress: 95,
totalSteps: 8,
completedSteps: 7,
remainingSteps: 1,
estimatedTimeRemaining: '15 minutos',
overallScore: 87
};
const sectionReview = [
{
id: 'business-info',
title: 'Información del Negocio',
status: 'completed',
score: 98,
items: [
{ field: 'Nombre de la panadería', value: 'Panadería Artesanal El Buen Pan', status: 'complete' },
{ field: 'Dirección', value: 'Av. Principal 123, Centro', status: 'complete' },
{ field: 'Teléfono', value: '+1 234 567 8900', status: 'complete' },
{ field: 'Email', value: 'info@elbuenpan.com', status: 'complete' },
{ field: 'Tipo de negocio', value: 'Panadería y Pastelería Artesanal', status: 'complete' },
{ field: 'Horario de operación', value: 'L-V: 6:00-20:00, S-D: 7:00-18:00', status: 'complete' }
],
recommendations: []
},
{
id: 'menu-products',
title: 'Menú y Productos',
status: 'completed',
score: 85,
items: [
{ field: 'Productos de panadería', value: '24 productos configurados', status: 'complete' },
{ field: 'Productos de pastelería', value: '18 productos configurados', status: 'complete' },
{ field: 'Bebidas', value: '12 opciones disponibles', status: 'complete' },
{ field: 'Precios configurados', value: '51 de 54 productos (94%)', status: 'warning' },
{ field: 'Categorías organizadas', value: '6 categorías principales', status: 'complete' },
{ field: 'Descripciones', value: '48 de 54 productos (89%)', status: 'warning' }
],
recommendations: [
'Completar precios para 3 productos pendientes',
'Añadir descripciones para 6 productos restantes'
]
},
{
id: 'inventory',
title: 'Inventario Inicial',
status: 'completed',
score: 92,
items: [
{ field: 'Materias primas', value: '45 ingredientes registrados', status: 'complete' },
{ field: 'Proveedores', value: '8 proveedores configurados', status: 'complete' },
{ field: 'Stocks iniciales', value: '43 de 45 ingredientes (96%)', status: 'warning' },
{ field: 'Puntos de reorden', value: '40 de 45 ingredientes (89%)', status: 'warning' },
{ field: 'Costos unitarios', value: 'Completado al 100%', status: 'complete' }
],
recommendations: [
'Definir stocks iniciales para 2 ingredientes',
'Establecer puntos de reorden para 5 ingredientes'
]
},
{
id: 'staff-config',
title: 'Configuración de Personal',
status: 'completed',
score: 90,
items: [
{ field: 'Empleados registrados', value: '12 miembros del equipo', status: 'complete' },
{ field: 'Roles asignados', value: '5 roles diferentes configurados', status: 'complete' },
{ field: 'Horarios de trabajo', value: '11 de 12 empleados (92%)', status: 'warning' },
{ field: 'Permisos del sistema', value: 'Configurado completamente', status: 'complete' },
{ field: 'Datos de contacto', value: 'Completado al 100%', status: 'complete' }
],
recommendations: [
'Completar horario para 1 empleado pendiente'
]
},
{
id: 'operations',
title: 'Configuración Operativa',
status: 'completed',
score: 95,
items: [
{ field: 'Horarios de operación', value: 'Configurado completamente', status: 'complete' },
{ field: 'Métodos de pago', value: '4 métodos activos', status: 'complete' },
{ field: 'Políticas de devoluciones', value: 'Definidas y configuradas', status: 'complete' },
{ field: 'Configuración de impuestos', value: '18% IVA configurado', status: 'complete' },
{ field: 'Zonas de entrega', value: '3 zonas configuradas', status: 'complete' }
],
recommendations: []
},
{
id: 'integrations',
title: 'Integraciones',
status: 'completed',
score: 88,
items: [
{ field: 'Sistema de pagos', value: 'Stripe configurado', status: 'complete' },
{ field: 'Notificaciones por email', value: 'SMTP configurado', status: 'complete' },
{ field: 'Notificaciones SMS', value: 'Twilio configurado', status: 'complete' },
{ field: 'Backup automático', value: 'Configurado diariamente', status: 'complete' },
{ field: 'APIs externas', value: '2 de 3 configuradas', status: 'warning' }
],
recommendations: [
'Configurar API de delivery restante'
]
},
{
id: 'testing',
title: 'Pruebas del Sistema',
status: 'pending',
score: 0,
items: [
{ field: 'Prueba de ventas', value: 'Pendiente', status: 'pending' },
{ field: 'Prueba de inventario', value: 'Pendiente', status: 'pending' },
{ field: 'Prueba de reportes', value: 'Pendiente', status: 'pending' },
{ field: 'Prueba de notificaciones', value: 'Pendiente', status: 'pending' },
{ field: 'Validación de integraciones', value: 'Pendiente', status: 'pending' }
],
recommendations: [
'Ejecutar todas las pruebas del sistema antes del lanzamiento'
]
},
{
id: 'training',
title: 'Capacitación del Equipo',
status: 'completed',
score: 82,
items: [
{ field: 'Capacitación básica', value: '10 de 12 empleados (83%)', status: 'warning' },
{ field: 'Manual del usuario', value: 'Entregado y revisado', status: 'complete' },
{ field: 'Sesiones prácticas', value: '2 de 3 sesiones completadas', status: 'warning' },
{ field: 'Evaluaciones', value: '8 de 10 empleados aprobados', status: 'warning' },
{ field: 'Material de referencia', value: 'Disponible en el sistema', status: 'complete' }
],
recommendations: [
'Completar capacitación para 2 empleados pendientes',
'Programar tercera sesión práctica',
'Realizar evaluaciones pendientes'
]
}
];
const overallRecommendations = [
{
priority: 'high',
category: 'Crítico',
title: 'Completar Pruebas del Sistema',
description: 'Ejecutar todas las pruebas funcionales antes del lanzamiento',
estimatedTime: '30 minutos',
impact: 'Garantiza funcionamiento correcto del sistema'
},
{
priority: 'medium',
category: 'Importante',
title: 'Finalizar Configuración de Productos',
description: 'Completar precios y descripciones pendientes',
estimatedTime: '20 minutos',
impact: 'Permite ventas completas de todos los productos'
},
{
priority: 'medium',
category: 'Importante',
title: 'Completar Capacitación del Personal',
description: 'Finalizar entrenamiento para empleados pendientes',
estimatedTime: '45 minutos',
impact: 'Asegura operación eficiente desde el primer día'
},
{
priority: 'low',
category: 'Opcional',
title: 'Optimizar Configuración de Inventario',
description: 'Definir stocks y puntos de reorden pendientes',
estimatedTime: '15 minutos',
impact: 'Mejora control automático de inventario'
}
];
const launchReadiness = {
essential: {
completed: 6,
total: 7,
percentage: 86
},
recommended: {
completed: 8,
total: 12,
percentage: 67
},
optional: {
completed: 3,
total: 6,
percentage: 50
}
};
const getStatusIcon = (status: string) => {
const iconProps = { className: "w-5 h-5" };
switch (status) {
case 'completed': return <CheckCircle {...iconProps} className="w-5 h-5 text-[var(--color-success)]" />;
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
case 'pending': return <Clock {...iconProps} className="w-5 h-5 text-[var(--text-secondary)]" />;
default: return <AlertCircle {...iconProps} />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'green';
case 'warning': return 'yellow';
case 'pending': return 'gray';
default: return 'red';
}
};
const getItemStatusIcon = (status: string) => {
const iconProps = { className: "w-4 h-4" };
switch (status) {
case 'complete': return <CheckCircle {...iconProps} className="w-4 h-4 text-[var(--color-success)]" />;
case 'warning': return <AlertCircle {...iconProps} className="w-4 h-4 text-yellow-600" />;
case 'pending': return <Clock {...iconProps} className="w-4 h-4 text-[var(--text-secondary)]" />;
default: return <AlertCircle {...iconProps} />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getScoreColor = (score: number) => {
if (score >= 90) return 'text-[var(--color-success)]';
if (score >= 80) return 'text-yellow-600';
if (score >= 70) return 'text-[var(--color-primary)]';
return 'text-[var(--color-error)]';
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Revisión Final de Configuración"
description="Verifica todos los aspectos de tu configuración antes del lanzamiento"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Edit2 className="w-4 h-4 mr-2" />
Editar Configuración
</Button>
<Button>
<Zap className="w-4 h-4 mr-2" />
Lanzar Sistema
</Button>
</div>
}
/>
{/* Overall Progress */}
<Card className="p-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="text-center">
<div className="relative w-20 h-20 mx-auto mb-3">
<div className="absolute inset-0 flex items-center justify-center">
<span className={`text-2xl font-bold ${getScoreColor(completionData.overallScore)}`}>
{completionData.overallScore}
</span>
</div>
<svg className="w-20 h-20 transform -rotate-90">
<circle
cx="40"
cy="40"
r="32"
stroke="currentColor"
strokeWidth="6"
fill="none"
className="text-gray-200"
/>
<circle
cx="40"
cy="40"
r="32"
stroke="currentColor"
strokeWidth="6"
fill="none"
strokeDasharray={`${completionData.overallScore * 2.01} 201`}
className="text-[var(--color-success)]"
/>
</svg>
</div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Puntuación General</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-[var(--color-info)]">{completionData.overallProgress}%</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Progreso Total</p>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${completionData.overallProgress}%` }}
></div>
</div>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-purple-600">
{completionData.completedSteps}/{completionData.totalSteps}
</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Secciones Completadas</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-[var(--color-primary)]">{completionData.estimatedTimeRemaining}</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Tiempo Restante</p>
</div>
</div>
</Card>
{/* Navigation Tabs */}
<div className="border-b border-[var(--border-primary)]">
<nav className="-mb-px flex space-x-8">
{['overview', 'sections', 'recommendations', 'readiness'].map((tab) => (
<button
key={tab}
onClick={() => setActiveSection(tab)}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeSection === tab
? 'border-blue-500 text-[var(--color-info)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
{tab === 'overview' && 'Resumen General'}
{tab === 'sections' && 'Revisión por Secciones'}
{tab === 'recommendations' && 'Recomendaciones'}
{tab === 'readiness' && 'Preparación para Lanzamiento'}
</button>
))}
</nav>
</div>
{/* Content based on active section */}
{activeSection === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Estado por Secciones</h3>
<div className="space-y-3">
{sectionReview.map((section) => (
<div key={section.id} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center space-x-3">
{getStatusIcon(section.status)}
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">{section.title}</p>
<p className="text-xs text-[var(--text-tertiary)]">
{section.recommendations.length > 0
? `${section.recommendations.length} recomendación${section.recommendations.length > 1 ? 'es' : ''}`
: 'Completado correctamente'
}
</p>
</div>
</div>
<div className="text-right">
<p className={`text-sm font-medium ${getScoreColor(section.score)}`}>
{section.score}%
</p>
<Badge variant={getStatusColor(section.status)}>
{section.status === 'completed' ? 'Completado' :
section.status === 'warning' ? 'Con observaciones' : 'Pendiente'}
</Badge>
</div>
</div>
))}
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Próximos Pasos</h3>
<div className="space-y-4">
{overallRecommendations.slice(0, 3).map((rec, index) => (
<div key={index} className="flex items-start space-x-3 p-3 border rounded-lg">
<Badge variant={getPriorityColor(rec.priority)} className="mt-1">
{rec.category}
</Badge>
<div className="flex-1">
<h4 className="text-sm font-medium text-[var(--text-primary)]">{rec.title}</h4>
<p className="text-sm text-[var(--text-secondary)] mt-1">{rec.description}</p>
<div className="flex items-center space-x-4 mt-2 text-xs text-[var(--text-tertiary)]">
<span> {rec.estimatedTime}</span>
<span>💡 {rec.impact}</span>
</div>
</div>
<ArrowRight className="w-4 h-4 text-[var(--text-tertiary)] mt-1" />
</div>
))}
</div>
<div className="mt-6 p-4 bg-[var(--color-info)]/5 rounded-lg">
<div className="flex items-center space-x-2 mb-2">
<Star className="w-5 h-5 text-[var(--color-info)]" />
<h4 className="font-medium text-blue-900">¡Excelente progreso!</h4>
</div>
<p className="text-sm text-[var(--color-info)]">
Has completado el 95% de la configuración. Solo quedan algunos detalles finales antes del lanzamiento.
</p>
</div>
</Card>
</div>
)}
{activeSection === 'sections' && (
<div className="space-y-6">
{sectionReview.map((section) => (
<Card key={section.id} className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
{getStatusIcon(section.status)}
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{section.title}</h3>
<Badge variant={getStatusColor(section.status)}>
Puntuación: {section.score}%
</Badge>
</div>
<Button variant="outline" size="sm">
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{section.items.map((item, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center space-x-2">
{getItemStatusIcon(item.status)}
<span className="text-sm font-medium text-[var(--text-secondary)]">{item.field}</span>
</div>
<span className="text-sm text-[var(--text-secondary)]">{item.value}</span>
</div>
))}
</div>
{section.recommendations.length > 0 && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<h4 className="text-sm font-medium text-yellow-800 mb-2">Recomendaciones:</h4>
<ul className="text-sm text-yellow-700 space-y-1">
{section.recommendations.map((rec, index) => (
<li key={index} className="flex items-start">
<span className="mr-2"></span>
<span>{rec}</span>
</li>
))}
</ul>
</div>
)}
</Card>
))}
</div>
)}
{activeSection === 'recommendations' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Todas las Recomendaciones</h3>
<div className="space-y-4">
{overallRecommendations.map((rec, index) => (
<div key={index} className="flex items-start justify-between p-4 border rounded-lg">
<div className="flex items-start space-x-3 flex-1">
<Badge variant={getPriorityColor(rec.priority)}>
{rec.category}
</Badge>
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-1">{rec.title}</h4>
<p className="text-sm text-[var(--text-secondary)] mb-2">{rec.description}</p>
<div className="flex items-center space-x-4 text-xs text-[var(--text-tertiary)]">
<span> Tiempo estimado: {rec.estimatedTime}</span>
<span>💡 Impacto: {rec.impact}</span>
</div>
</div>
</div>
<div className="flex space-x-2">
<Button size="sm" variant="outline">Más Información</Button>
<Button size="sm">Completar</Button>
</div>
</div>
))}
</div>
</Card>
)}
{activeSection === 'readiness' && (
<div className="space-y-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Preparación para el Lanzamiento</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div className="text-center p-4 border rounded-lg">
<div className="w-16 h-16 mx-auto mb-3 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<CheckCircle className="w-8 h-8 text-[var(--color-success)]" />
</div>
<h4 className="font-medium text-[var(--text-primary)] mb-2">Elementos Esenciales</h4>
<p className="text-2xl font-bold text-[var(--color-success)] mb-1">
{launchReadiness.essential.completed}/{launchReadiness.essential.total}
</p>
<p className="text-sm text-[var(--text-secondary)]">{launchReadiness.essential.percentage}% completado</p>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
<div
className="bg-green-600 h-2 rounded-full"
style={{ width: `${launchReadiness.essential.percentage}%` }}
></div>
</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="w-16 h-16 mx-auto mb-3 bg-yellow-100 rounded-full flex items-center justify-center">
<Star className="w-8 h-8 text-yellow-600" />
</div>
<h4 className="font-medium text-[var(--text-primary)] mb-2">Recomendados</h4>
<p className="text-2xl font-bold text-yellow-600 mb-1">
{launchReadiness.recommended.completed}/{launchReadiness.recommended.total}
</p>
<p className="text-sm text-[var(--text-secondary)]">{launchReadiness.recommended.percentage}% completado</p>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
<div
className="bg-yellow-600 h-2 rounded-full"
style={{ width: `${launchReadiness.recommended.percentage}%` }}
></div>
</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="w-16 h-16 mx-auto mb-3 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Users className="w-8 h-8 text-[var(--color-info)]" />
</div>
<h4 className="font-medium text-[var(--text-primary)] mb-2">Opcionales</h4>
<p className="text-2xl font-bold text-[var(--color-info)] mb-1">
{launchReadiness.optional.completed}/{launchReadiness.optional.total}
</p>
<p className="text-sm text-[var(--text-secondary)]">{launchReadiness.optional.percentage}% completado</p>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${launchReadiness.optional.percentage}%` }}
></div>
</div>
</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-6">
<div className="flex items-center space-x-3 mb-4">
<CheckCircle className="w-6 h-6 text-[var(--color-success)]" />
<h4 className="text-lg font-semibold text-green-900">¡Listo para Lanzar!</h4>
</div>
<p className="text-[var(--color-success)] mb-4">
Tu configuración está lista para el lanzamiento. Todos los elementos esenciales están completados
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"
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>
<Button variant="outline">
Ejecutar Pruebas Finales
</Button>
</div>
</div>
</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>
);
};
export default OnboardingReviewPage;

View File

@@ -1,579 +0,0 @@
import React, { useState } from 'react';
import { CheckCircle, AlertCircle, Edit2, Eye, Star, ArrowRight, Clock, Users, Zap } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingReviewPage: React.FC = () => {
const [activeSection, setActiveSection] = useState<string>('overview');
const completionData = {
overallProgress: 95,
totalSteps: 8,
completedSteps: 7,
remainingSteps: 1,
estimatedTimeRemaining: '15 minutos',
overallScore: 87
};
const sectionReview = [
{
id: 'business-info',
title: 'Información del Negocio',
status: 'completed',
score: 98,
items: [
{ field: 'Nombre de la panadería', value: 'Panadería Artesanal El Buen Pan', status: 'complete' },
{ field: 'Dirección', value: 'Av. Principal 123, Centro', status: 'complete' },
{ field: 'Teléfono', value: '+1 234 567 8900', status: 'complete' },
{ field: 'Email', value: 'info@elbuenpan.com', status: 'complete' },
{ field: 'Tipo de negocio', value: 'Panadería y Pastelería Artesanal', status: 'complete' },
{ field: 'Horario de operación', value: 'L-V: 6:00-20:00, S-D: 7:00-18:00', status: 'complete' }
],
recommendations: []
},
{
id: 'menu-products',
title: 'Menú y Productos',
status: 'completed',
score: 85,
items: [
{ field: 'Productos de panadería', value: '24 productos configurados', status: 'complete' },
{ field: 'Productos de pastelería', value: '18 productos configurados', status: 'complete' },
{ field: 'Bebidas', value: '12 opciones disponibles', status: 'complete' },
{ field: 'Precios configurados', value: '51 de 54 productos (94%)', status: 'warning' },
{ field: 'Categorías organizadas', value: '6 categorías principales', status: 'complete' },
{ field: 'Descripciones', value: '48 de 54 productos (89%)', status: 'warning' }
],
recommendations: [
'Completar precios para 3 productos pendientes',
'Añadir descripciones para 6 productos restantes'
]
},
{
id: 'inventory',
title: 'Inventario Inicial',
status: 'completed',
score: 92,
items: [
{ field: 'Materias primas', value: '45 ingredientes registrados', status: 'complete' },
{ field: 'Proveedores', value: '8 proveedores configurados', status: 'complete' },
{ field: 'Stocks iniciales', value: '43 de 45 ingredientes (96%)', status: 'warning' },
{ field: 'Puntos de reorden', value: '40 de 45 ingredientes (89%)', status: 'warning' },
{ field: 'Costos unitarios', value: 'Completado al 100%', status: 'complete' }
],
recommendations: [
'Definir stocks iniciales para 2 ingredientes',
'Establecer puntos de reorden para 5 ingredientes'
]
},
{
id: 'staff-config',
title: 'Configuración de Personal',
status: 'completed',
score: 90,
items: [
{ field: 'Empleados registrados', value: '12 miembros del equipo', status: 'complete' },
{ field: 'Roles asignados', value: '5 roles diferentes configurados', status: 'complete' },
{ field: 'Horarios de trabajo', value: '11 de 12 empleados (92%)', status: 'warning' },
{ field: 'Permisos del sistema', value: 'Configurado completamente', status: 'complete' },
{ field: 'Datos de contacto', value: 'Completado al 100%', status: 'complete' }
],
recommendations: [
'Completar horario para 1 empleado pendiente'
]
},
{
id: 'operations',
title: 'Configuración Operativa',
status: 'completed',
score: 95,
items: [
{ field: 'Horarios de operación', value: 'Configurado completamente', status: 'complete' },
{ field: 'Métodos de pago', value: '4 métodos activos', status: 'complete' },
{ field: 'Políticas de devoluciones', value: 'Definidas y configuradas', status: 'complete' },
{ field: 'Configuración de impuestos', value: '18% IVA configurado', status: 'complete' },
{ field: 'Zonas de entrega', value: '3 zonas configuradas', status: 'complete' }
],
recommendations: []
},
{
id: 'integrations',
title: 'Integraciones',
status: 'completed',
score: 88,
items: [
{ field: 'Sistema de pagos', value: 'Stripe configurado', status: 'complete' },
{ field: 'Notificaciones por email', value: 'SMTP configurado', status: 'complete' },
{ field: 'Notificaciones SMS', value: 'Twilio configurado', status: 'complete' },
{ field: 'Backup automático', value: 'Configurado diariamente', status: 'complete' },
{ field: 'APIs externas', value: '2 de 3 configuradas', status: 'warning' }
],
recommendations: [
'Configurar API de delivery restante'
]
},
{
id: 'testing',
title: 'Pruebas del Sistema',
status: 'pending',
score: 0,
items: [
{ field: 'Prueba de ventas', value: 'Pendiente', status: 'pending' },
{ field: 'Prueba de inventario', value: 'Pendiente', status: 'pending' },
{ field: 'Prueba de reportes', value: 'Pendiente', status: 'pending' },
{ field: 'Prueba de notificaciones', value: 'Pendiente', status: 'pending' },
{ field: 'Validación de integraciones', value: 'Pendiente', status: 'pending' }
],
recommendations: [
'Ejecutar todas las pruebas del sistema antes del lanzamiento'
]
},
{
id: 'training',
title: 'Capacitación del Equipo',
status: 'completed',
score: 82,
items: [
{ field: 'Capacitación básica', value: '10 de 12 empleados (83%)', status: 'warning' },
{ field: 'Manual del usuario', value: 'Entregado y revisado', status: 'complete' },
{ field: 'Sesiones prácticas', value: '2 de 3 sesiones completadas', status: 'warning' },
{ field: 'Evaluaciones', value: '8 de 10 empleados aprobados', status: 'warning' },
{ field: 'Material de referencia', value: 'Disponible en el sistema', status: 'complete' }
],
recommendations: [
'Completar capacitación para 2 empleados pendientes',
'Programar tercera sesión práctica',
'Realizar evaluaciones pendientes'
]
}
];
const overallRecommendations = [
{
priority: 'high',
category: 'Crítico',
title: 'Completar Pruebas del Sistema',
description: 'Ejecutar todas las pruebas funcionales antes del lanzamiento',
estimatedTime: '30 minutos',
impact: 'Garantiza funcionamiento correcto del sistema'
},
{
priority: 'medium',
category: 'Importante',
title: 'Finalizar Configuración de Productos',
description: 'Completar precios y descripciones pendientes',
estimatedTime: '20 minutos',
impact: 'Permite ventas completas de todos los productos'
},
{
priority: 'medium',
category: 'Importante',
title: 'Completar Capacitación del Personal',
description: 'Finalizar entrenamiento para empleados pendientes',
estimatedTime: '45 minutos',
impact: 'Asegura operación eficiente desde el primer día'
},
{
priority: 'low',
category: 'Opcional',
title: 'Optimizar Configuración de Inventario',
description: 'Definir stocks y puntos de reorden pendientes',
estimatedTime: '15 minutos',
impact: 'Mejora control automático de inventario'
}
];
const launchReadiness = {
essential: {
completed: 6,
total: 7,
percentage: 86
},
recommended: {
completed: 8,
total: 12,
percentage: 67
},
optional: {
completed: 3,
total: 6,
percentage: 50
}
};
const getStatusIcon = (status: string) => {
const iconProps = { className: "w-5 h-5" };
switch (status) {
case 'completed': return <CheckCircle {...iconProps} className="w-5 h-5 text-green-600" />;
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
case 'pending': return <Clock {...iconProps} className="w-5 h-5 text-gray-600" />;
default: return <AlertCircle {...iconProps} />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'green';
case 'warning': return 'yellow';
case 'pending': return 'gray';
default: return 'red';
}
};
const getItemStatusIcon = (status: string) => {
const iconProps = { className: "w-4 h-4" };
switch (status) {
case 'complete': return <CheckCircle {...iconProps} className="w-4 h-4 text-green-600" />;
case 'warning': return <AlertCircle {...iconProps} className="w-4 h-4 text-yellow-600" />;
case 'pending': return <Clock {...iconProps} className="w-4 h-4 text-gray-600" />;
default: return <AlertCircle {...iconProps} />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getScoreColor = (score: number) => {
if (score >= 90) return 'text-green-600';
if (score >= 80) return 'text-yellow-600';
if (score >= 70) return 'text-orange-600';
return 'text-red-600';
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Revisión Final de Configuración"
description="Verifica todos los aspectos de tu configuración antes del lanzamiento"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Edit2 className="w-4 h-4 mr-2" />
Editar Configuración
</Button>
<Button>
<Zap className="w-4 h-4 mr-2" />
Lanzar Sistema
</Button>
</div>
}
/>
{/* Overall Progress */}
<Card className="p-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="text-center">
<div className="relative w-20 h-20 mx-auto mb-3">
<div className="absolute inset-0 flex items-center justify-center">
<span className={`text-2xl font-bold ${getScoreColor(completionData.overallScore)}`}>
{completionData.overallScore}
</span>
</div>
<svg className="w-20 h-20 transform -rotate-90">
<circle
cx="40"
cy="40"
r="32"
stroke="currentColor"
strokeWidth="6"
fill="none"
className="text-gray-200"
/>
<circle
cx="40"
cy="40"
r="32"
stroke="currentColor"
strokeWidth="6"
fill="none"
strokeDasharray={`${completionData.overallScore * 2.01} 201`}
className="text-green-600"
/>
</svg>
</div>
<p className="text-sm font-medium text-gray-700">Puntuación General</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-blue-600">{completionData.overallProgress}%</p>
<p className="text-sm font-medium text-gray-700">Progreso Total</p>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${completionData.overallProgress}%` }}
></div>
</div>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-purple-600">
{completionData.completedSteps}/{completionData.totalSteps}
</p>
<p className="text-sm font-medium text-gray-700">Secciones Completadas</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-orange-600">{completionData.estimatedTimeRemaining}</p>
<p className="text-sm font-medium text-gray-700">Tiempo Restante</p>
</div>
</div>
</Card>
{/* Navigation Tabs */}
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
{['overview', 'sections', 'recommendations', 'readiness'].map((tab) => (
<button
key={tab}
onClick={() => setActiveSection(tab)}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeSection === tab
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab === 'overview' && 'Resumen General'}
{tab === 'sections' && 'Revisión por Secciones'}
{tab === 'recommendations' && 'Recomendaciones'}
{tab === 'readiness' && 'Preparación para Lanzamiento'}
</button>
))}
</nav>
</div>
{/* Content based on active section */}
{activeSection === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Estado por Secciones</h3>
<div className="space-y-3">
{sectionReview.map((section) => (
<div key={section.id} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center space-x-3">
{getStatusIcon(section.status)}
<div>
<p className="text-sm font-medium text-gray-900">{section.title}</p>
<p className="text-xs text-gray-500">
{section.recommendations.length > 0
? `${section.recommendations.length} recomendación${section.recommendations.length > 1 ? 'es' : ''}`
: 'Completado correctamente'
}
</p>
</div>
</div>
<div className="text-right">
<p className={`text-sm font-medium ${getScoreColor(section.score)}`}>
{section.score}%
</p>
<Badge variant={getStatusColor(section.status)}>
{section.status === 'completed' ? 'Completado' :
section.status === 'warning' ? 'Con observaciones' : 'Pendiente'}
</Badge>
</div>
</div>
))}
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Próximos Pasos</h3>
<div className="space-y-4">
{overallRecommendations.slice(0, 3).map((rec, index) => (
<div key={index} className="flex items-start space-x-3 p-3 border rounded-lg">
<Badge variant={getPriorityColor(rec.priority)} className="mt-1">
{rec.category}
</Badge>
<div className="flex-1">
<h4 className="text-sm font-medium text-gray-900">{rec.title}</h4>
<p className="text-sm text-gray-600 mt-1">{rec.description}</p>
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-500">
<span>⏱️ {rec.estimatedTime}</span>
<span>💡 {rec.impact}</span>
</div>
</div>
<ArrowRight className="w-4 h-4 text-gray-400 mt-1" />
</div>
))}
</div>
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
<div className="flex items-center space-x-2 mb-2">
<Star className="w-5 h-5 text-blue-600" />
<h4 className="font-medium text-blue-900">¡Excelente progreso!</h4>
</div>
<p className="text-sm text-blue-800">
Has completado el 95% de la configuración. Solo quedan algunos detalles finales antes del lanzamiento.
</p>
</div>
</Card>
</div>
)}
{activeSection === 'sections' && (
<div className="space-y-6">
{sectionReview.map((section) => (
<Card key={section.id} className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
{getStatusIcon(section.status)}
<h3 className="text-lg font-semibold text-gray-900">{section.title}</h3>
<Badge variant={getStatusColor(section.status)}>
Puntuación: {section.score}%
</Badge>
</div>
<Button variant="outline" size="sm">
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{section.items.map((item, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-2">
{getItemStatusIcon(item.status)}
<span className="text-sm font-medium text-gray-700">{item.field}</span>
</div>
<span className="text-sm text-gray-600">{item.value}</span>
</div>
))}
</div>
{section.recommendations.length > 0 && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<h4 className="text-sm font-medium text-yellow-800 mb-2">Recomendaciones:</h4>
<ul className="text-sm text-yellow-700 space-y-1">
{section.recommendations.map((rec, index) => (
<li key={index} className="flex items-start">
<span className="mr-2">•</span>
<span>{rec}</span>
</li>
))}
</ul>
</div>
)}
</Card>
))}
</div>
)}
{activeSection === 'recommendations' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Todas las Recomendaciones</h3>
<div className="space-y-4">
{overallRecommendations.map((rec, index) => (
<div key={index} className="flex items-start justify-between p-4 border rounded-lg">
<div className="flex items-start space-x-3 flex-1">
<Badge variant={getPriorityColor(rec.priority)}>
{rec.category}
</Badge>
<div>
<h4 className="text-sm font-medium text-gray-900 mb-1">{rec.title}</h4>
<p className="text-sm text-gray-600 mb-2">{rec.description}</p>
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span>⏱️ Tiempo estimado: {rec.estimatedTime}</span>
<span>💡 Impacto: {rec.impact}</span>
</div>
</div>
</div>
<div className="flex space-x-2">
<Button size="sm" variant="outline">Más Información</Button>
<Button size="sm">Completar</Button>
</div>
</div>
))}
</div>
</Card>
)}
{activeSection === 'readiness' && (
<div className="space-y-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Preparación para el Lanzamiento</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div className="text-center p-4 border rounded-lg">
<div className="w-16 h-16 mx-auto mb-3 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<h4 className="font-medium text-gray-900 mb-2">Elementos Esenciales</h4>
<p className="text-2xl font-bold text-green-600 mb-1">
{launchReadiness.essential.completed}/{launchReadiness.essential.total}
</p>
<p className="text-sm text-gray-600">{launchReadiness.essential.percentage}% completado</p>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div
className="bg-green-600 h-2 rounded-full"
style={{ width: `${launchReadiness.essential.percentage}%` }}
></div>
</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="w-16 h-16 mx-auto mb-3 bg-yellow-100 rounded-full flex items-center justify-center">
<Star className="w-8 h-8 text-yellow-600" />
</div>
<h4 className="font-medium text-gray-900 mb-2">Recomendados</h4>
<p className="text-2xl font-bold text-yellow-600 mb-1">
{launchReadiness.recommended.completed}/{launchReadiness.recommended.total}
</p>
<p className="text-sm text-gray-600">{launchReadiness.recommended.percentage}% completado</p>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div
className="bg-yellow-600 h-2 rounded-full"
style={{ width: `${launchReadiness.recommended.percentage}%` }}
></div>
</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="w-16 h-16 mx-auto mb-3 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="w-8 h-8 text-blue-600" />
</div>
<h4 className="font-medium text-gray-900 mb-2">Opcionales</h4>
<p className="text-2xl font-bold text-blue-600 mb-1">
{launchReadiness.optional.completed}/{launchReadiness.optional.total}
</p>
<p className="text-sm text-gray-600">{launchReadiness.optional.percentage}% completado</p>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${launchReadiness.optional.percentage}%` }}
></div>
</div>
</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-6">
<div className="flex items-center space-x-3 mb-4">
<CheckCircle className="w-6 h-6 text-green-600" />
<h4 className="text-lg font-semibold text-green-900">¡Listo para Lanzar!</h4>
</div>
<p className="text-green-800 mb-4">
Tu configuración está lista para el lanzamiento. Todos los elementos esenciales están completados
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">
<Zap className="w-4 h-4 mr-2" />
Lanzar Ahora
</Button>
<Button variant="outline">
Ejecutar Pruebas Finales
</Button>
</div>
</div>
</Card>
</div>
)}
</div>
);
};
export default OnboardingReviewPage;

View File

@@ -1 +0,0 @@
export { default as OnboardingReviewPage } from './OnboardingReviewPage';

View File

@@ -1,906 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import { ChevronRight, ChevronLeft, Check, Store, Upload, Brain } from 'lucide-react';
import { Button, Card, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingSetupPage: React.FC = () => {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState({
bakery: {
name: 'Panadería Artesanal El Buen Pan',
type: 'artisan',
size: 'medium',
location: 'Av. Principal 123, Centro Histórico',
phone: '+1 234 567 8900',
email: 'info@elbuenpan.com'
},
upload: {
salesData: null,
inventoryData: null,
recipeData: null
}
});
const [trainingState, setTrainingState] = useState({
isTraining: false,
progress: 0,
currentStep: 'Iniciando entrenamiento...',
status: 'pending', // 'pending', 'running', 'completed', 'error'
logs: [] as string[]
});
const wsRef = useRef<WebSocket | null>(null);
const steps = [
{
id: 1,
title: 'Información de la Panadería',
description: 'Detalles básicos sobre tu negocio',
icon: Store,
fields: ['name', 'type', 'location', 'contact']
},
{
id: 2,
title: 'Carga de Datos',
description: 'Importa tus datos existentes para acelerar la configuración inicial',
icon: Upload,
fields: ['files', 'templates', 'validation']
},
{
id: 3,
title: 'Entrenamiento IA',
description: 'Configurando tu modelo de inteligencia artificial personalizado',
icon: Brain,
fields: ['training', 'progress', 'completion']
}
];
const bakeryTypes = [
{
value: 'artisan',
label: 'Panadería Artesanal Local',
description: 'Producción propia y tradicional en el local'
},
{
value: 'dependent',
label: 'Panadería Dependiente',
description: 'Dependiente de un panadero central'
}
];
const handleInputChange = (section: string, field: string, value: any) => {
setFormData(prev => ({
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: value
}
}));
};
const handleArrayToggle = (section: string, field: string, value: string) => {
setFormData(prev => {
const currentArray = prev[section as keyof typeof prev][field] || [];
const newArray = currentArray.includes(value)
? currentArray.filter((item: string) => item !== value)
: [...currentArray, value];
return {
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: newArray
}
};
});
};
const nextStep = () => {
if (currentStep < steps.length) {
setCurrentStep(currentStep + 1);
}
};
const prevStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const handleFinish = () => {
console.log('Onboarding completed:', formData);
// Navigate to dashboard - onboarding is complete
window.location.href = '/app/dashboard';
};
const downloadTemplate = (type: 'sales' | 'inventory' | 'recipes') => {
let csvContent = '';
let fileName = '';
switch (type) {
case 'sales':
csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
2024-01-15,Pan Integral,5,2.50,12.50,Cliente A,Tienda
2024-01-15,Croissant,3,1.80,5.40,Cliente B,Online
2024-01-15,Baguette,2,3.00,6.00,Cliente C,Tienda
2024-01-16,Pan de Centeno,4,2.80,11.20,Cliente A,Tienda
2024-01-16,Empanadas,6,4.50,27.00,Cliente D,Delivery`;
fileName = 'plantilla_ventas.csv';
break;
case 'inventory':
csvContent = `nombre,categoria,unidad_medida,stock_actual,stock_minimo,stock_maximo,precio_compra,proveedor,fecha_vencimiento
Harina de Trigo,Ingrediente,kg,50,20,100,1.20,Molinos del Sur,2024-12-31
Azúcar Blanca,Ingrediente,kg,25,10,50,0.85,Dulces SA,2024-12-31
Levadura Fresca,Ingrediente,kg,5,2,10,3.50,Levaduras Pro,2024-03-15
Mantequilla,Ingrediente,kg,15,5,30,4.20,Lácteos Premium,2024-02-28
Pan Integral,Producto Final,unidad,20,10,50,0.00,Producción Propia,2024-01-20`;
fileName = 'plantilla_inventario.csv';
break;
case 'recipes':
csvContent = `nombre_receta,categoria,tiempo_preparacion,tiempo_coccion,porciones,ingrediente,cantidad,unidad
Pan Integral,Panadería,30,45,2,Harina Integral,500,g
Pan Integral,Panadería,30,45,2,Agua,325,ml
Pan Integral,Panadería,30,45,2,Sal,10,g
Pan Integral,Panadería,30,45,2,Levadura,7,g
Croissant,Bollería,120,20,12,Harina,400,g
Croissant,Bollería,120,20,12,Mantequilla,250,g
Croissant,Bollería,120,20,12,Agua,200,ml
Croissant,Bollería,120,20,12,Sal,8,g`;
fileName = 'plantilla_recetas.csv';
break;
}
// Create and download the file
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', fileName);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// WebSocket connection for ML training updates
const connectWebSocket = () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
return;
}
const wsUrl = process.env.NODE_ENV === 'production'
? 'wss://api.bakeryai.com/ws/training'
: 'ws://localhost:8000/ws/training';
wsRef.current = new WebSocket(wsUrl);
wsRef.current.onopen = () => {
console.log('WebSocket connected for ML training');
setTrainingState(prev => ({ ...prev, status: 'running' }));
};
wsRef.current.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
setTrainingState(prev => ({
...prev,
progress: data.progress || prev.progress,
currentStep: data.step || prev.currentStep,
status: data.status || prev.status,
logs: data.log ? [...prev.logs, data.log] : prev.logs
}));
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
wsRef.current.onclose = () => {
console.log('WebSocket connection closed');
};
wsRef.current.onerror = (error) => {
console.error('WebSocket error:', error);
setTrainingState(prev => ({ ...prev, status: 'error' }));
};
};
const startTraining = () => {
setTrainingState(prev => ({ ...prev, isTraining: true, progress: 0 }));
connectWebSocket();
// Send training configuration to server
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({
action: 'start_training',
bakery_data: formData.bakery,
upload_data: formData.upload
}));
}
};
// Cleanup WebSocket on unmount
useEffect(() => {
return () => {
if (wsRef.current) {
wsRef.current.close();
}
};
}, []);
const isStepComplete = (stepId: number) => {
// Basic validation logic
switch (stepId) {
case 1:
return formData.bakery.name && formData.bakery.location;
case 2:
return true; // Upload step is now optional - users can skip if they want
case 3:
return trainingState.status === 'completed';
default:
return false;
}
};
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<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>
<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-lg font-semibold text-[var(--text-primary)] mb-4">
Tipo de Panadería *
</label>
<div className="space-y-4">
{bakeryTypes.map((type) => (
<label
key={type.value}
className={`
group relative flex p-6 border-2 rounded-xl cursor-pointer transition-all duration-300 hover:scale-[1.02] 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="flex items-start space-x-4 w-full">
<div className="relative flex-shrink-0 mt-1">
<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-6 h-6 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-3 h-3 bg-white rounded-full"></div>
)}
</div>
</div>
<div className="flex-1">
<h3 className={`
text-lg font-semibold mb-2 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}
</h3>
<p className={`text-sm transition-colors duration-300 ${
formData.bakery.type === type.value ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)] group-hover:text-[var(--text-secondary)]'
}`}>
{type.description}
</p>
</div>
</div>
{/* Selection indicator */}
{formData.bakery.type === type.value && (
<div className="absolute top-4 right-4">
<Check className="w-5 h-5 text-[var(--color-primary)]" />
</div>
)}
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Ubicación *
</label>
<Input
value={formData.bakery.location}
onChange={(e) => handleInputChange('bakery', 'location', e.target.value)}
placeholder="Dirección completa"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Teléfono
</label>
<Input
value={formData.bakery.phone}
onChange={(e) => handleInputChange('bakery', 'phone', e.target.value)}
placeholder="+34 xxx xxx xxx"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Email
</label>
<Input
value={formData.bakery.email}
onChange={(e) => handleInputChange('bakery', 'email', e.target.value)}
placeholder="contacto@panaderia.com"
/>
</div>
</div>
</div>
);
case 2:
return (
<div className="space-y-8">
{/* Upload Options */}
<div className="space-y-4">
<div className={`grid grid-cols-1 gap-4 ${
formData.bakery.type === 'dependent' ? 'md:grid-cols-2' : 'md:grid-cols-3'
}`}>
{/* 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-3">
Historial de ventas (CSV, Excel)
</p>
<div className="text-xs text-[var(--text-tertiary)] mb-4 bg-[var(--bg-secondary)] rounded-lg p-3 text-left">
<div className="font-medium mb-1">Columnas requeridas:</div>
<div> <strong>Fecha</strong> (date, fecha)</div>
<div> <strong>Producto</strong> (product, producto)</div>
<div className="font-medium mt-2 mb-1">Columnas opcionales:</div>
<div> Cantidad, Precio, Categoría, Ubicación</div>
<div className="mt-2 text-[var(--color-info)]">Formatos: CSV, Excel | Máx: 10MB</div>
</div>
<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 - Only for artisan bakeries */}
{formData.bakery.type === 'artisan' && (
<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>
{/* Template Downloads */}
<div className="space-y-4 pt-6 border-t border-[var(--border-secondary)]">
<div className="text-center">
<h4 className="font-semibold text-[var(--text-primary)] mb-2">¿Necesitas ayuda con el formato?</h4>
<p className="text-sm text-[var(--text-secondary)] mb-4">
Descarga nuestras plantillas para estructurar correctamente tus datos
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<button
onClick={() => downloadTemplate('sales')}
className="flex items-center justify-center px-4 py-2 border border-[var(--color-success)] text-[var(--color-success)] rounded-lg hover:bg-[var(--color-success)]/10 transition-all duration-200"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Plantilla de Ventas
</button>
<button
onClick={() => downloadTemplate('inventory')}
className="flex items-center justify-center px-4 py-2 border border-[var(--color-warning)] text-[var(--color-warning)] rounded-lg hover:bg-[var(--color-warning)]/10 transition-all duration-200"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Plantilla de Inventario
</button>
{/* Recipe template - Only for artisan bakeries */}
{formData.bakery.type === 'artisan' && (
<button
onClick={() => downloadTemplate('recipes')}
className="flex items-center justify-center px-4 py-2 border border-[var(--color-info)] text-[var(--color-info)] rounded-lg hover:bg-[var(--color-info)]/10 transition-all duration-200"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Plantilla de Recetas
</button>
)}
</div>
</div>
</div>
</div>
);
case 3:
return (
<div className="space-y-8">
{/* Training Progress */}
{!trainingState.isTraining && trainingState.status === 'pending' && (
<div className="text-center">
<p className="text-[var(--text-secondary)] mb-6">
Presiona el botón para iniciar el entrenamiento de tu modelo de IA
</p>
<Button
onClick={startTraining}
className="px-8 py-3 bg-[var(--color-info)] hover:bg-[var(--color-info)]/80 text-white"
>
<Brain className="w-4 h-4 mr-2" />
Iniciar Entrenamiento
</Button>
</div>
)}
{/* Training in Progress */}
{trainingState.isTraining && (
<div className="space-y-6">
{/* Progress Bar */}
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-[var(--text-primary)]">
Progreso del Entrenamiento
</span>
<span className="text-sm text-[var(--text-secondary)]">
{trainingState.progress}%
</span>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-4 overflow-hidden">
<div
className="bg-gradient-to-r from-[var(--color-info)] to-[var(--color-primary)] h-full rounded-full transition-all duration-500 ease-out relative"
style={{ width: `${trainingState.progress}%` }}
>
<div className="absolute inset-0 bg-white opacity-20 rounded-full"></div>
{trainingState.progress > 0 && (
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-pulse rounded-full"></div>
)}
</div>
</div>
</div>
{/* Current Step */}
<div className="flex items-center space-x-3 p-4 bg-[var(--bg-secondary)] rounded-lg border">
<div className="w-3 h-3 bg-[var(--color-info)] rounded-full animate-pulse"></div>
<span className="text-[var(--text-primary)] font-medium">
{trainingState.currentStep}
</span>
</div>
{/* Training Status */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className={`p-4 rounded-lg border text-center ${
trainingState.progress >= 25
? 'bg-[var(--color-success)]/10 border-[var(--color-success)]'
: 'bg-[var(--bg-secondary)] border-[var(--border-secondary)]'
}`}>
<div className={`w-8 h-8 mx-auto mb-2 rounded-full flex items-center justify-center ${
trainingState.progress >= 25 ? 'bg-[var(--color-success)]' : 'bg-[var(--bg-tertiary)]'
}`}>
{trainingState.progress >= 25 ? (
<Check className="w-4 h-4 text-white" />
) : (
<span className="text-xs text-[var(--text-tertiary)]">1</span>
)}
</div>
<p className="text-xs font-medium">Carga de Datos</p>
</div>
<div className={`p-4 rounded-lg border text-center ${
trainingState.progress >= 75
? 'bg-[var(--color-success)]/10 border-[var(--color-success)]'
: 'bg-[var(--bg-secondary)] border-[var(--border-secondary)]'
}`}>
<div className={`w-8 h-8 mx-auto mb-2 rounded-full flex items-center justify-center ${
trainingState.progress >= 75 ? 'bg-[var(--color-success)]' : 'bg-[var(--bg-tertiary)]'
}`}>
{trainingState.progress >= 75 ? (
<Check className="w-4 h-4 text-white" />
) : (
<span className="text-xs text-[var(--text-tertiary)]">2</span>
)}
</div>
<p className="text-xs font-medium">Entrenamiento</p>
</div>
<div className={`p-4 rounded-lg border text-center ${
trainingState.status === 'completed'
? 'bg-[var(--color-success)]/10 border-[var(--color-success)]'
: 'bg-[var(--bg-secondary)] border-[var(--border-secondary)]'
}`}>
<div className={`w-8 h-8 mx-auto mb-2 rounded-full flex items-center justify-center ${
trainingState.status === 'completed' ? 'bg-[var(--color-success)]' : 'bg-[var(--bg-tertiary)]'
}`}>
{trainingState.status === 'completed' ? (
<Check className="w-4 h-4 text-white" />
) : (
<span className="text-xs text-[var(--text-tertiary)]">3</span>
)}
</div>
<p className="text-xs font-medium">Validación</p>
</div>
</div>
{/* Training Logs */}
{trainingState.logs.length > 0 && (
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
Log de Entrenamiento
</h4>
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 max-h-32 overflow-y-auto border">
{trainingState.logs.slice(-5).map((log, index) => (
<div key={index} className="text-xs text-[var(--text-secondary)] mb-1 font-mono">
{log}
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Training Completed */}
{trainingState.status === 'completed' && (
<div className="text-center p-6 bg-[var(--color-success)]/10 rounded-xl border border-[var(--color-success)]/20">
<Check className="w-12 h-12 text-[var(--color-success)] mx-auto mb-4" />
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
¡Entrenamiento Completado!
</h4>
<p className="text-[var(--text-secondary)]">
Tu modelo de IA personalizado está listo para ayudarte a optimizar tu panadería
</p>
</div>
)}
{/* Training Error */}
{trainingState.status === 'error' && (
<div className="text-center p-6 bg-[var(--color-error)]/10 rounded-xl border border-[var(--color-error)]/20">
<div className="w-12 h-12 bg-[var(--color-error)] rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-white text-xl">!</span>
</div>
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Error en el Entrenamiento
</h4>
<p className="text-[var(--text-secondary)] mb-4">
Ocurrió un problema durante el entrenamiento. Por favor, inténtalo de nuevo.
</p>
<Button
onClick={startTraining}
className="px-6 py-2 bg-[var(--color-info)] hover:bg-[var(--color-info)]/80 text-white"
>
Reintentar
</Button>
</div>
)}
</div>
);
default:
return null;
}
};
return (
<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>
<div className="max-w-4xl mx-auto">
{/* Single Integrated Card */}
<Card className="shadow-lg overflow-hidden">
{/* Progress Header Inside Card */}
<div className="bg-[var(--bg-secondary)] p-4 sm:p-6 border-b border-[var(--border-secondary)]">
{/* Step Indicators - Mobile Optimized */}
<div className="mb-4 sm:mb-6">
{/* Mobile: Vertical Step List */}
<div className="block sm:hidden space-y-3">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center space-x-3">
<div className={`
w-8 h-8 rounded-full flex items-center justify-center text-xs font-medium transition-all duration-300 flex-shrink-0
${step.id <= currentStep
? 'bg-[var(--color-primary)] text-white'
: 'bg-[var(--bg-quaternary)] text-[var(--text-tertiary)] border border-[var(--border-secondary)]'
}
`}>
{step.id < currentStep ? (
<Check className="w-3 h-3" />
) : step.id === currentStep ? (
<step.icon className="w-3 h-3" />
) : (
<step.icon className="w-3 h-3" />
)}
</div>
<div className="flex-1">
<h3 className={`text-sm font-medium ${
step.id === currentStep
? 'text-[var(--color-primary)]'
: step.id < currentStep
? 'text-[var(--color-success)]'
: 'text-[var(--text-tertiary)]'
}`}>
{step.title}
</h3>
{step.id === currentStep && (
<p className="text-xs text-[var(--text-secondary)] mt-1">
{step.description}
</p>
)}
</div>
</div>
))}
</div>
{/* Desktop: Horizontal Step Indicators */}
<div className="hidden sm:flex items-center justify-center space-x-6">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center">
<div className="flex flex-col items-center">
<div className={`
w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-all duration-300 shadow-sm
${step.id <= currentStep
? 'bg-[var(--color-primary)] text-white'
: 'bg-[var(--bg-quaternary)] text-[var(--text-tertiary)] border border-[var(--border-secondary)]'
}
`}>
{step.id < currentStep ? (
<Check className="w-4 h-4" />
) : step.id === currentStep ? (
<step.icon className="w-4 h-4" />
) : (
<step.icon className="w-4 h-4" />
)}
</div>
<p className={`text-xs mt-2 text-center max-w-16 leading-tight ${
step.id === currentStep
? 'text-[var(--color-primary)] font-semibold'
: 'text-[var(--text-tertiary)]'
}`}>
{step.title}
</p>
</div>
{/* Connection line - Desktop only */}
{index < steps.length - 1 && (
<div className={`
w-16 h-0.5 mx-3 transition-all duration-500
${step.id < currentStep ? 'bg-[var(--color-primary)]' : 'bg-[var(--border-secondary)]'}
`} />
)}
</div>
))}
</div>
</div>
{/* Step Info - Desktop Only (Mobile shows inline) */}
<div className="text-center hidden sm:block">
<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)] mb-4">
{steps[currentStep - 1].description}
</p>
</div>
{/* Mobile Step Info */}
<div className="text-center block sm:hidden mb-4">
<h2 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
{steps[currentStep - 1].title}
</h2>
</div>
{/* Progress Bar */}
<div className="relative max-w-sm sm:max-w-md mx-auto">
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 overflow-hidden">
<div
className="bg-[var(--color-primary)] h-full rounded-full transition-all duration-700 ease-out"
style={{ width: `${(currentStep / steps.length) * 100}%` }}
/>
</div>
<div className="text-center mt-2">
<span className="text-sm font-medium text-[var(--color-primary)]">
{Math.round((currentStep / steps.length) * 100)}% Completado
</span>
</div>
</div>
</div>
{/* Form Content */}
<div className="p-4 sm:p-8">
<div className="space-y-4 sm:space-y-6">
{renderStepContent()}
</div>
</div>
{/* Navigation Footer */}
<div className="bg-[var(--bg-secondary)] px-4 sm:px-8 py-4 sm:py-6 border-t border-[var(--border-secondary)]">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 sm:gap-0">
{/* Mobile: Full width buttons stacked */}
<div className="flex w-full sm:w-auto gap-3 sm:hidden">
<Button
variant="outline"
onClick={prevStep}
disabled={currentStep === 1}
className="flex-1 py-3 disabled:opacity-50"
>
<ChevronLeft className="w-4 h-4 mr-2" />
Anterior
</Button>
{currentStep === steps.length ? (
<Button
onClick={handleFinish}
disabled={!isStepComplete(currentStep)}
className="flex-1 py-3 bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white disabled:opacity-50"
>
<Check className="w-4 h-4 mr-2" />
Finalizar
</Button>
) : (
<Button
onClick={nextStep}
disabled={!isStepComplete(currentStep)}
className="flex-1 py-3 disabled:opacity-50"
>
Siguiente
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
)}
</div>
{/* Desktop: Original layout */}
<Button
variant="outline"
onClick={prevStep}
disabled={currentStep === 1}
className="hidden sm:flex px-6 py-3 disabled:opacity-50"
>
<ChevronLeft className="w-4 h-4 mr-2" />
Anterior
</Button>
{/* Step indicators - smaller on mobile */}
<div className="flex items-center space-x-1.5 sm:space-x-2">
{steps.map((_, index) => (
<div
key={index}
className={`
w-1.5 h-1.5 sm:w-2 sm:h-2 rounded-full transition-all duration-300
${index + 1 === currentStep
? 'bg-[var(--color-primary)] scale-125'
: index + 1 < currentStep
? 'bg-[var(--color-success)]'
: 'bg-[var(--border-secondary)]'
}
`}
/>
))}
</div>
{currentStep === steps.length ? (
<Button
onClick={handleFinish}
disabled={!isStepComplete(currentStep)}
className="hidden sm:flex px-8 py-3 bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white disabled:opacity-50"
>
<Check className="w-4 h-4 mr-2" />
Finalizar
</Button>
) : (
<Button
onClick={nextStep}
disabled={!isStepComplete(currentStep)}
className="hidden sm:flex px-6 py-3 disabled:opacity-50"
>
Siguiente
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
)}
</div>
</div>
</Card>
</div>
</div>
);
};
export default OnboardingSetupPage;

View File

@@ -1,499 +0,0 @@
import React, { useState } from 'react';
import { ChevronRight, ChevronLeft, Check, Store, Users, Settings, Zap } from 'lucide-react';
import { Button, Card, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingSetupPage: React.FC = () => {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState({
bakery: {
name: '',
type: 'traditional',
size: 'medium',
location: '',
phone: '',
email: ''
},
team: {
ownerName: '',
teamSize: '5-10',
roles: [],
experience: 'intermediate'
},
operations: {
openingHours: {
start: '07:00',
end: '20:00'
},
daysOpen: 6,
specialties: [],
dailyProduction: 'medium'
},
goals: {
primaryGoals: [],
expectedRevenue: '',
timeline: '6months'
}
});
const steps = [
{
id: 1,
title: 'Información de la Panadería',
description: 'Detalles básicos sobre tu negocio',
icon: Store,
fields: ['name', 'type', 'location', 'contact']
},
{
id: 2,
title: 'Equipo y Personal',
description: 'Información sobre tu equipo de trabajo',
icon: Users,
fields: ['owner', 'teamSize', 'roles', 'experience']
},
{
id: 3,
title: 'Operaciones',
description: 'Horarios y especialidades de producción',
icon: Settings,
fields: ['hours', 'specialties', 'production']
},
{
id: 4,
title: 'Objetivos',
description: 'Metas y expectativas para tu panadería',
icon: Zap,
fields: ['goals', 'revenue', 'timeline']
}
];
const bakeryTypes = [
{ value: 'traditional', label: 'Panadería Tradicional' },
{ value: 'artisan', label: 'Panadería Artesanal' },
{ value: 'cafe', label: 'Panadería-Café' },
{ value: 'industrial', label: 'Producción Industrial' }
];
const specialties = [
{ value: 'bread', label: 'Pan Tradicional' },
{ value: 'pastries', label: 'Bollería' },
{ value: 'cakes', label: 'Tartas y Pasteles' },
{ value: 'cookies', label: 'Galletas' },
{ value: 'savory', label: 'Productos Salados' },
{ value: 'gluten-free', label: 'Sin Gluten' },
{ value: 'vegan', label: 'Vegano' },
{ value: 'organic', label: 'Orgánico' }
];
const businessGoals = [
{ value: 'increase-sales', label: 'Aumentar Ventas' },
{ value: 'reduce-waste', label: 'Reducir Desperdicios' },
{ value: 'improve-efficiency', label: 'Mejorar Eficiencia' },
{ value: 'expand-menu', label: 'Ampliar Menú' },
{ value: 'digital-presence', label: 'Presencia Digital' },
{ value: 'customer-loyalty', label: 'Fidelización de Clientes' }
];
const handleInputChange = (section: string, field: string, value: any) => {
setFormData(prev => ({
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: value
}
}));
};
const handleArrayToggle = (section: string, field: string, value: string) => {
setFormData(prev => {
const currentArray = prev[section as keyof typeof prev][field] || [];
const newArray = currentArray.includes(value)
? currentArray.filter((item: string) => item !== value)
: [...currentArray, value];
return {
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: newArray
}
};
});
};
const nextStep = () => {
if (currentStep < steps.length) {
setCurrentStep(currentStep + 1);
}
};
const prevStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const handleFinish = () => {
console.log('Onboarding completed:', formData);
// Handle completion logic
};
const isStepComplete = (stepId: number) => {
// Basic validation logic
switch (stepId) {
case 1:
return formData.bakery.name && formData.bakery.location;
case 2:
return formData.team.ownerName;
case 3:
return formData.operations.specialties.length > 0;
case 4:
return formData.goals.primaryGoals.length > 0;
default:
return false;
}
};
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
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>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
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-gray-50">
<input
type="radio"
name="bakeryType"
value={type.value}
checked={formData.bakery.type === type.value}
onChange={(e) => handleInputChange('bakery', 'type', e.target.value)}
className="text-blue-600"
/>
<span className="text-sm">{type.label}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Ubicación *
</label>
<Input
value={formData.bakery.location}
onChange={(e) => handleInputChange('bakery', 'location', e.target.value)}
placeholder="Dirección completa"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Teléfono
</label>
<Input
value={formData.bakery.phone}
onChange={(e) => handleInputChange('bakery', 'phone', e.target.value)}
placeholder="+34 xxx xxx xxx"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email
</label>
<Input
value={formData.bakery.email}
onChange={(e) => handleInputChange('bakery', 'email', e.target.value)}
placeholder="contacto@panaderia.com"
/>
</div>
</div>
</div>
);
case 2:
return (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nombre del Propietario *
</label>
<Input
value={formData.team.ownerName}
onChange={(e) => handleInputChange('team', 'ownerName', e.target.value)}
placeholder="Tu nombre completo"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tamaño del Equipo
</label>
<select
value={formData.team.teamSize}
onChange={(e) => handleInputChange('team', 'teamSize', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="1-2">Solo yo o 1-2 personas</option>
<option value="3-5">3-5 empleados</option>
<option value="5-10">5-10 empleados</option>
<option value="10+">Más de 10 empleados</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Experiencia en el Sector
</label>
<div className="space-y-2">
{[
{ value: 'beginner', label: 'Principiante (menos de 2 años)' },
{ value: 'intermediate', label: 'Intermedio (2-5 años)' },
{ value: 'experienced', label: 'Experimentado (5-10 años)' },
{ value: 'expert', label: 'Experto (más de 10 años)' }
].map((exp) => (
<label key={exp.value} className="flex items-center space-x-3">
<input
type="radio"
name="experience"
value={exp.value}
checked={formData.team.experience === exp.value}
onChange={(e) => handleInputChange('team', 'experience', e.target.value)}
className="text-blue-600"
/>
<span className="text-sm">{exp.label}</span>
</label>
))}
</div>
</div>
</div>
);
case 3:
return (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Hora de Apertura
</label>
<input
type="time"
value={formData.operations.openingHours.start}
onChange={(e) => handleInputChange('operations', 'openingHours', {
...formData.operations.openingHours,
start: e.target.value
})}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Hora de Cierre
</label>
<input
type="time"
value={formData.operations.openingHours.end}
onChange={(e) => handleInputChange('operations', 'openingHours', {
...formData.operations.openingHours,
end: e.target.value
})}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Días de Operación por Semana
</label>
<select
value={formData.operations.daysOpen}
onChange={(e) => handleInputChange('operations', 'daysOpen', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value={5}>5 días</option>
<option value={6}>6 días</option>
<option value={7}>7 días</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Especialidades *
</label>
<div className="grid grid-cols-2 gap-3">
{specialties.map((specialty) => (
<label key={specialty.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="checkbox"
checked={formData.operations.specialties.includes(specialty.value)}
onChange={() => handleArrayToggle('operations', 'specialties', specialty.value)}
className="text-blue-600 rounded"
/>
<span className="text-sm">{specialty.label}</span>
</label>
))}
</div>
</div>
</div>
);
case 4:
return (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Objetivos Principales *
</label>
<div className="grid grid-cols-2 gap-3">
{businessGoals.map((goal) => (
<label key={goal.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="checkbox"
checked={formData.goals.primaryGoals.includes(goal.value)}
onChange={() => handleArrayToggle('goals', 'primaryGoals', goal.value)}
className="text-blue-600 rounded"
/>
<span className="text-sm">{goal.label}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Ingresos Mensuales Esperados (opcional)
</label>
<select
value={formData.goals.expectedRevenue}
onChange={(e) => handleInputChange('goals', 'expectedRevenue', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="">Seleccionar rango</option>
<option value="0-5000">Menos de €5,000</option>
<option value="5000-15000">€5,000 - €15,000</option>
<option value="15000-30000">€15,000 - €30,000</option>
<option value="30000+">Más de €30,000</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Plazo para Alcanzar Objetivos
</label>
<select
value={formData.goals.timeline}
onChange={(e) => handleInputChange('goals', 'timeline', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="3months">3 meses</option>
<option value="6months">6 meses</option>
<option value="1year">1 año</option>
<option value="2years">2 años o más</option>
</select>
</div>
</div>
);
default:
return null;
}
};
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"
/>
{/* 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-gray-200 text-gray-600'
}`}>
{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-gray-200'
}`} />
)}
</div>
))}
</div>
<div className="text-center">
<h2 className="text-xl font-semibold text-gray-900 mb-2">
Paso {currentStep}: {steps[currentStep - 1].title}
</h2>
<p className="text-gray-600">
{steps[currentStep - 1].description}
</p>
</div>
</Card>
{/* Step Content */}
<Card className="p-8 mb-8">
{renderStepContent()}
</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>
) : (
<Button
onClick={nextStep}
disabled={!isStepComplete(currentStep)}
>
Siguiente
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
)}
</div>
</div>
);
};
export default OnboardingSetupPage;

View File

@@ -1 +0,0 @@
export { default as OnboardingSetupPage } from './OnboardingSetupPage';

View File

@@ -1,454 +0,0 @@
import React, { useState } from 'react';
import { Upload, File, Check, AlertCircle, Download, RefreshCw, Trash2, Eye } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingUploadPage: React.FC = () => {
const [dragActive, setDragActive] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [isProcessing, setIsProcessing] = useState(false);
const uploadedFiles = [
{
id: '1',
name: 'productos_menu.csv',
type: 'productos',
size: '45 KB',
status: 'completed',
uploadedAt: '2024-01-26 10:30:00',
records: 127,
errors: 3,
warnings: 8
},
{
id: '2',
name: 'inventario_inicial.xlsx',
type: 'inventario',
size: '82 KB',
status: 'completed',
uploadedAt: '2024-01-26 10:25:00',
records: 89,
errors: 0,
warnings: 2
},
{
id: '3',
name: 'empleados.csv',
type: 'empleados',
size: '12 KB',
status: 'processing',
uploadedAt: '2024-01-26 10:35:00',
records: 8,
errors: 0,
warnings: 0
},
{
id: '4',
name: 'ventas_historicas.csv',
type: 'ventas',
size: '256 KB',
status: 'error',
uploadedAt: '2024-01-26 10:20:00',
records: 0,
errors: 1,
warnings: 0,
errorMessage: 'Formato de fecha incorrecto en la columna "fecha_venta"'
}
];
const supportedFormats = [
{
type: 'productos',
name: 'Productos y Menú',
formats: ['CSV', 'Excel'],
description: 'Lista de productos con precios, categorías y descripciones',
template: 'template_productos.csv',
requiredFields: ['nombre', 'categoria', 'precio', 'descripcion']
},
{
type: 'inventario',
name: 'Inventario Inicial',
formats: ['CSV', 'Excel'],
description: 'Stock inicial de ingredientes y materias primas',
template: 'template_inventario.xlsx',
requiredFields: ['item', 'cantidad', 'unidad', 'costo_unitario']
},
{
type: 'empleados',
name: 'Empleados',
formats: ['CSV'],
description: 'Información del personal y roles',
template: 'template_empleados.csv',
requiredFields: ['nombre', 'cargo', 'email', 'telefono']
},
{
type: 'ventas',
name: 'Historial de Ventas',
formats: ['CSV'],
description: 'Datos históricos de ventas para análisis',
template: 'template_ventas.csv',
requiredFields: ['fecha', 'producto', 'cantidad', 'total']
},
{
type: 'proveedores',
name: 'Proveedores',
formats: ['CSV', 'Excel'],
description: 'Lista de proveedores y datos de contacto',
template: 'template_proveedores.csv',
requiredFields: ['nombre', 'contacto', 'telefono', 'email']
}
];
const uploadStats = {
totalFiles: uploadedFiles.length,
completedFiles: uploadedFiles.filter(f => f.status === 'completed').length,
totalRecords: uploadedFiles.reduce((sum, f) => sum + f.records, 0),
totalErrors: uploadedFiles.reduce((sum, f) => sum + f.errors, 0),
totalWarnings: uploadedFiles.reduce((sum, f) => sum + f.warnings, 0)
};
const getStatusIcon = (status: string) => {
const iconProps = { className: "w-5 h-5" };
switch (status) {
case 'completed': return <Check {...iconProps} className="w-5 h-5 text-[var(--color-success)]" />;
case 'processing': return <RefreshCw {...iconProps} className="w-5 h-5 text-[var(--color-info)] animate-spin" />;
case 'error': return <AlertCircle {...iconProps} className="w-5 h-5 text-[var(--color-error)]" />;
default: return <File {...iconProps} />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'green';
case 'processing': return 'blue';
case 'error': return 'red';
default: return 'gray';
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case 'completed': return 'Completado';
case 'processing': return 'Procesando';
case 'error': return 'Error';
default: return status;
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
const files = Array.from(e.dataTransfer.files);
handleFiles(files);
};
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const files = Array.from(e.target.files);
handleFiles(files);
}
};
const handleFiles = (files: File[]) => {
console.log('Files selected:', files);
// Simulate upload progress
setIsProcessing(true);
setUploadProgress(0);
const interval = setInterval(() => {
setUploadProgress(prev => {
if (prev >= 100) {
clearInterval(interval);
setIsProcessing(false);
return 100;
}
return prev + 10;
});
}, 200);
};
const downloadTemplate = (template: string) => {
console.log('Downloading template:', template);
// Handle template download
};
const retryUpload = (fileId: string) => {
console.log('Retrying upload for file:', fileId);
// Handle retry logic
};
const deleteFile = (fileId: string) => {
console.log('Deleting file:', fileId);
// Handle delete logic
};
const viewDetails = (fileId: string) => {
console.log('Viewing details for file:', fileId);
// Handle view details logic
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Carga de Datos"
description="Importa tus datos existentes para acelerar la configuración inicial"
/>
{/* Upload Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Archivos Subidos</p>
<p className="text-3xl font-bold text-[var(--color-info)]">{uploadStats.totalFiles}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Upload className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Completados</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{uploadStats.completedFiles}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<Check className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Registros</p>
<p className="text-3xl font-bold text-purple-600">{uploadStats.totalRecords}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<File className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Errores</p>
<p className="text-3xl font-bold text-[var(--color-error)]">{uploadStats.totalErrors}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
<AlertCircle className="h-6 w-6 text-[var(--color-error)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Advertencias</p>
<p className="text-3xl font-bold text-yellow-600">{uploadStats.totalWarnings}</p>
</div>
<div className="h-12 w-12 bg-yellow-100 rounded-full flex items-center justify-center">
<AlertCircle className="h-6 w-6 text-yellow-600" />
</div>
</div>
</Card>
</div>
{/* Upload Area */}
<Card className="p-8">
<div
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
dragActive
? 'border-blue-400 bg-[var(--color-info)]/5'
: 'border-[var(--border-secondary)] hover:border-[var(--border-tertiary)]'
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<Upload className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Arrastra archivos aquí o haz clic para seleccionar
</h3>
<p className="text-[var(--text-secondary)] mb-4">
Formatos soportados: CSV, Excel (XLSX). Tamaño máximo: 10MB por archivo
</p>
<input
type="file"
multiple
accept=".csv,.xlsx,.xls"
onChange={handleFileInput}
className="hidden"
id="file-upload"
/>
<label htmlFor="file-upload">
<Button className="cursor-pointer">
Seleccionar Archivos
</Button>
</label>
{isProcessing && (
<div className="mt-4">
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mb-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
<p className="text-sm text-[var(--text-secondary)]">Procesando... {uploadProgress}%</p>
</div>
)}
</div>
</Card>
{/* Supported Formats */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Formatos Soportados</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{supportedFormats.map((format, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-[var(--text-primary)]">{format.name}</h4>
<div className="flex space-x-1">
{format.formats.map((fmt, idx) => (
<Badge key={idx} variant="blue" className="text-xs">{fmt}</Badge>
))}
</div>
</div>
<p className="text-sm text-[var(--text-secondary)] mb-3">{format.description}</p>
<div className="mb-3">
<p className="text-xs font-medium text-[var(--text-secondary)] mb-1">Campos requeridos:</p>
<div className="flex flex-wrap gap-1">
{format.requiredFields.map((field, idx) => (
<span key={idx} className="px-2 py-1 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] text-xs rounded">
{field}
</span>
))}
</div>
</div>
<Button
size="sm"
variant="outline"
className="w-full"
onClick={() => downloadTemplate(format.template)}
>
<Download className="w-3 h-3 mr-2" />
Descargar Plantilla
</Button>
</div>
))}
</div>
</Card>
{/* Uploaded Files */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Archivos Cargados</h3>
<div className="space-y-3">
{uploadedFiles.map((file) => (
<div key={file.id} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center space-x-4 flex-1">
{getStatusIcon(file.status)}
<div>
<div className="flex items-center space-x-3 mb-1">
<h4 className="text-sm font-medium text-[var(--text-primary)]">{file.name}</h4>
<Badge variant={getStatusColor(file.status)}>
{getStatusLabel(file.status)}
</Badge>
<span className="text-xs text-[var(--text-tertiary)]">{file.size}</span>
</div>
<div className="flex items-center space-x-4 text-xs text-[var(--text-secondary)]">
<span>{file.records} registros</span>
{file.errors > 0 && (
<span className="text-[var(--color-error)]">{file.errors} errores</span>
)}
{file.warnings > 0 && (
<span className="text-yellow-600">{file.warnings} advertencias</span>
)}
<span>Subido: {new Date(file.uploadedAt).toLocaleString('es-ES')}</span>
</div>
{file.status === 'error' && file.errorMessage && (
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-[var(--color-error)]">
{file.errorMessage}
</div>
)}
</div>
</div>
<div className="flex space-x-2">
<Button size="sm" variant="outline" onClick={() => viewDetails(file.id)}>
<Eye className="w-3 h-3" />
</Button>
{file.status === 'error' && (
<Button size="sm" variant="outline" onClick={() => retryUpload(file.id)}>
<RefreshCw className="w-3 h-3" />
</Button>
)}
<Button size="sm" variant="outline" onClick={() => deleteFile(file.id)}>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
))}
</div>
</Card>
{/* Help Section */}
<Card className="p-6 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h3 className="text-lg font-semibold text-blue-900 mb-4">Tips para la Carga de Datos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-[var(--color-info)]">
<ul className="space-y-2">
<li> Usa las plantillas proporcionadas para garantizar el formato correcto</li>
<li> Verifica que todos los campos requeridos estén completos</li>
<li> Los archivos CSV deben usar codificación UTF-8</li>
<li> Las fechas deben estar en formato DD/MM/YYYY</li>
</ul>
<ul className="space-y-2">
<li> Los precios deben usar punto (.) como separador decimal</li>
<li> Evita caracteres especiales en los nombres de productos</li>
<li> Mantén los nombres de archivos descriptivos</li>
<li> Puedes cargar múltiples archivos del mismo tipo</li>
</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>
);
};
export default OnboardingUploadPage;

View File

@@ -1,438 +0,0 @@
import React, { useState } from 'react';
import { Upload, File, Check, AlertCircle, Download, RefreshCw, Trash2, Eye } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingUploadPage: React.FC = () => {
const [dragActive, setDragActive] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [isProcessing, setIsProcessing] = useState(false);
const uploadedFiles = [
{
id: '1',
name: 'productos_menu.csv',
type: 'productos',
size: '45 KB',
status: 'completed',
uploadedAt: '2024-01-26 10:30:00',
records: 127,
errors: 3,
warnings: 8
},
{
id: '2',
name: 'inventario_inicial.xlsx',
type: 'inventario',
size: '82 KB',
status: 'completed',
uploadedAt: '2024-01-26 10:25:00',
records: 89,
errors: 0,
warnings: 2
},
{
id: '3',
name: 'empleados.csv',
type: 'empleados',
size: '12 KB',
status: 'processing',
uploadedAt: '2024-01-26 10:35:00',
records: 8,
errors: 0,
warnings: 0
},
{
id: '4',
name: 'ventas_historicas.csv',
type: 'ventas',
size: '256 KB',
status: 'error',
uploadedAt: '2024-01-26 10:20:00',
records: 0,
errors: 1,
warnings: 0,
errorMessage: 'Formato de fecha incorrecto en la columna "fecha_venta"'
}
];
const supportedFormats = [
{
type: 'productos',
name: 'Productos y Menú',
formats: ['CSV', 'Excel'],
description: 'Lista de productos con precios, categorías y descripciones',
template: 'template_productos.csv',
requiredFields: ['nombre', 'categoria', 'precio', 'descripcion']
},
{
type: 'inventario',
name: 'Inventario Inicial',
formats: ['CSV', 'Excel'],
description: 'Stock inicial de ingredientes y materias primas',
template: 'template_inventario.xlsx',
requiredFields: ['item', 'cantidad', 'unidad', 'costo_unitario']
},
{
type: 'empleados',
name: 'Empleados',
formats: ['CSV'],
description: 'Información del personal y roles',
template: 'template_empleados.csv',
requiredFields: ['nombre', 'cargo', 'email', 'telefono']
},
{
type: 'ventas',
name: 'Historial de Ventas',
formats: ['CSV'],
description: 'Datos históricos de ventas para análisis',
template: 'template_ventas.csv',
requiredFields: ['fecha', 'producto', 'cantidad', 'total']
},
{
type: 'proveedores',
name: 'Proveedores',
formats: ['CSV', 'Excel'],
description: 'Lista de proveedores y datos de contacto',
template: 'template_proveedores.csv',
requiredFields: ['nombre', 'contacto', 'telefono', 'email']
}
];
const uploadStats = {
totalFiles: uploadedFiles.length,
completedFiles: uploadedFiles.filter(f => f.status === 'completed').length,
totalRecords: uploadedFiles.reduce((sum, f) => sum + f.records, 0),
totalErrors: uploadedFiles.reduce((sum, f) => sum + f.errors, 0),
totalWarnings: uploadedFiles.reduce((sum, f) => sum + f.warnings, 0)
};
const getStatusIcon = (status: string) => {
const iconProps = { className: "w-5 h-5" };
switch (status) {
case 'completed': return <Check {...iconProps} className="w-5 h-5 text-green-600" />;
case 'processing': return <RefreshCw {...iconProps} className="w-5 h-5 text-blue-600 animate-spin" />;
case 'error': return <AlertCircle {...iconProps} className="w-5 h-5 text-red-600" />;
default: return <File {...iconProps} />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'green';
case 'processing': return 'blue';
case 'error': return 'red';
default: return 'gray';
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case 'completed': return 'Completado';
case 'processing': return 'Procesando';
case 'error': return 'Error';
default: return status;
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
const files = Array.from(e.dataTransfer.files);
handleFiles(files);
};
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const files = Array.from(e.target.files);
handleFiles(files);
}
};
const handleFiles = (files: File[]) => {
console.log('Files selected:', files);
// Simulate upload progress
setIsProcessing(true);
setUploadProgress(0);
const interval = setInterval(() => {
setUploadProgress(prev => {
if (prev >= 100) {
clearInterval(interval);
setIsProcessing(false);
return 100;
}
return prev + 10;
});
}, 200);
};
const downloadTemplate = (template: string) => {
console.log('Downloading template:', template);
// Handle template download
};
const retryUpload = (fileId: string) => {
console.log('Retrying upload for file:', fileId);
// Handle retry logic
};
const deleteFile = (fileId: string) => {
console.log('Deleting file:', fileId);
// Handle delete logic
};
const viewDetails = (fileId: string) => {
console.log('Viewing details for file:', fileId);
// Handle view details logic
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Carga de Datos"
description="Importa tus datos existentes para acelerar la configuración inicial"
/>
{/* Upload Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Archivos Subidos</p>
<p className="text-3xl font-bold text-blue-600">{uploadStats.totalFiles}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<Upload className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Completados</p>
<p className="text-3xl font-bold text-green-600">{uploadStats.completedFiles}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<Check className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Registros</p>
<p className="text-3xl font-bold text-purple-600">{uploadStats.totalRecords}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<File className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Errores</p>
<p className="text-3xl font-bold text-red-600">{uploadStats.totalErrors}</p>
</div>
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
<AlertCircle className="h-6 w-6 text-red-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Advertencias</p>
<p className="text-3xl font-bold text-yellow-600">{uploadStats.totalWarnings}</p>
</div>
<div className="h-12 w-12 bg-yellow-100 rounded-full flex items-center justify-center">
<AlertCircle className="h-6 w-6 text-yellow-600" />
</div>
</div>
</Card>
</div>
{/* Upload Area */}
<Card className="p-8">
<div
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
dragActive
? 'border-blue-400 bg-blue-50'
: 'border-gray-300 hover:border-gray-400'
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Arrastra archivos aquí o haz clic para seleccionar
</h3>
<p className="text-gray-600 mb-4">
Formatos soportados: CSV, Excel (XLSX). Tamaño máximo: 10MB por archivo
</p>
<input
type="file"
multiple
accept=".csv,.xlsx,.xls"
onChange={handleFileInput}
className="hidden"
id="file-upload"
/>
<label htmlFor="file-upload">
<Button className="cursor-pointer">
Seleccionar Archivos
</Button>
</label>
{isProcessing && (
<div className="mt-4">
<div className="w-full bg-gray-200 rounded-full h-2 mb-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
<p className="text-sm text-gray-600">Procesando... {uploadProgress}%</p>
</div>
)}
</div>
</Card>
{/* Supported Formats */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Formatos Soportados</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{supportedFormats.map((format, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-gray-900">{format.name}</h4>
<div className="flex space-x-1">
{format.formats.map((fmt, idx) => (
<Badge key={idx} variant="blue" className="text-xs">{fmt}</Badge>
))}
</div>
</div>
<p className="text-sm text-gray-600 mb-3">{format.description}</p>
<div className="mb-3">
<p className="text-xs font-medium text-gray-700 mb-1">Campos requeridos:</p>
<div className="flex flex-wrap gap-1">
{format.requiredFields.map((field, idx) => (
<span key={idx} className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded">
{field}
</span>
))}
</div>
</div>
<Button
size="sm"
variant="outline"
className="w-full"
onClick={() => downloadTemplate(format.template)}
>
<Download className="w-3 h-3 mr-2" />
Descargar Plantilla
</Button>
</div>
))}
</div>
</Card>
{/* Uploaded Files */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Archivos Cargados</h3>
<div className="space-y-3">
{uploadedFiles.map((file) => (
<div key={file.id} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center space-x-4 flex-1">
{getStatusIcon(file.status)}
<div>
<div className="flex items-center space-x-3 mb-1">
<h4 className="text-sm font-medium text-gray-900">{file.name}</h4>
<Badge variant={getStatusColor(file.status)}>
{getStatusLabel(file.status)}
</Badge>
<span className="text-xs text-gray-500">{file.size}</span>
</div>
<div className="flex items-center space-x-4 text-xs text-gray-600">
<span>{file.records} registros</span>
{file.errors > 0 && (
<span className="text-red-600">{file.errors} errores</span>
)}
{file.warnings > 0 && (
<span className="text-yellow-600">{file.warnings} advertencias</span>
)}
<span>Subido: {new Date(file.uploadedAt).toLocaleString('es-ES')}</span>
</div>
{file.status === 'error' && file.errorMessage && (
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700">
{file.errorMessage}
</div>
)}
</div>
</div>
<div className="flex space-x-2">
<Button size="sm" variant="outline" onClick={() => viewDetails(file.id)}>
<Eye className="w-3 h-3" />
</Button>
{file.status === 'error' && (
<Button size="sm" variant="outline" onClick={() => retryUpload(file.id)}>
<RefreshCw className="w-3 h-3" />
</Button>
)}
<Button size="sm" variant="outline" onClick={() => deleteFile(file.id)}>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
))}
</div>
</Card>
{/* Help Section */}
<Card className="p-6 bg-blue-50 border-blue-200">
<h3 className="text-lg font-semibold text-blue-900 mb-4">Tips para la Carga de Datos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-blue-800">
<ul className="space-y-2">
<li>• Usa las plantillas proporcionadas para garantizar el formato correcto</li>
<li>• Verifica que todos los campos requeridos estén completos</li>
<li>• Los archivos CSV deben usar codificación UTF-8</li>
<li>• Las fechas deben estar en formato DD/MM/YYYY</li>
</ul>
<ul className="space-y-2">
<li>• Los precios deben usar punto (.) como separador decimal</li>
<li>• Evita caracteres especiales en los nombres de productos</li>
<li>• Mantén los nombres de archivos descriptivos</li>
<li>• Puedes cargar múltiples archivos del mismo tipo</li>
</ul>
</div>
</Card>
</div>
);
};
export default OnboardingUploadPage;

View File

@@ -1 +0,0 @@
export { default as OnboardingUploadPage } from './OnboardingUploadPage';