Add onboarding flow improvements
This commit is contained in:
161
frontend/src/pages/app/onboarding/OnboardingPage.tsx
Normal file
161
frontend/src/pages/app/onboarding/OnboardingPage.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as OnboardingAnalysisPage } from './OnboardingAnalysisPage';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as OnboardingReviewPage } from './OnboardingReviewPage';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as OnboardingSetupPage } from './OnboardingSetupPage';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as OnboardingUploadPage } from './OnboardingUploadPage';
|
||||
Reference in New Issue
Block a user