ADD new frontend
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingAnalysisPage;
|
||||
@@ -0,0 +1,435 @@
|
||||
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
frontend/src/pages/app/onboarding/analysis/index.ts
Normal file
1
frontend/src/pages/app/onboarding/analysis/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as OnboardingAnalysisPage } from './OnboardingAnalysisPage';
|
||||
@@ -0,0 +1,579 @@
|
||||
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">
|
||||
<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;
|
||||
@@ -0,0 +1,579 @@
|
||||
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
frontend/src/pages/app/onboarding/review/index.ts
Normal file
1
frontend/src/pages/app/onboarding/review/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as OnboardingReviewPage } from './OnboardingReviewPage';
|
||||
499
frontend/src/pages/app/onboarding/setup/OnboardingSetupPage.tsx
Normal file
499
frontend/src/pages/app/onboarding/setup/OnboardingSetupPage.tsx
Normal file
@@ -0,0 +1,499 @@
|
||||
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-[var(--text-secondary)] 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-[var(--text-secondary)] 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-[var(--bg-secondary)]">
|
||||
<input
|
||||
type="radio"
|
||||
name="bakeryType"
|
||||
value={type.value}
|
||||
checked={formData.bakery.type === type.value}
|
||||
onChange={(e) => handleInputChange('bakery', 'type', e.target.value)}
|
||||
className="text-[var(--color-info)]"
|
||||
/>
|
||||
<span className="text-sm">{type.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</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-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] 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-[var(--text-secondary)] 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-[var(--border-secondary)] 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-[var(--text-secondary)] 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-[var(--color-info)]"
|
||||
/>
|
||||
<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-[var(--text-secondary)] 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-[var(--border-secondary)] rounded-md"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] 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-[var(--border-secondary)] rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] 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-[var(--border-secondary)] 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-[var(--text-secondary)] 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-[var(--bg-secondary)]">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.operations.specialties.includes(specialty.value)}
|
||||
onChange={() => handleArrayToggle('operations', 'specialties', specialty.value)}
|
||||
className="text-[var(--color-info)] 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-[var(--text-secondary)] 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-[var(--bg-secondary)]">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.goals.primaryGoals.includes(goal.value)}
|
||||
onChange={() => handleArrayToggle('goals', 'primaryGoals', goal.value)}
|
||||
className="text-[var(--color-info)] rounded"
|
||||
/>
|
||||
<span className="text-sm">{goal.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] 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-[var(--border-secondary)] 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-[var(--text-secondary)] 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-[var(--border-secondary)] 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-[var(--bg-quaternary)] text-[var(--text-secondary)]'
|
||||
}`}>
|
||||
{step.id < currentStep || (step.id === currentStep && isStepComplete(step.id)) ? (
|
||||
<Check className="w-5 h-5" />
|
||||
) : (
|
||||
<step.icon className="w-5 h-5" />
|
||||
)}
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className={`w-full h-1 mx-4 ${
|
||||
step.id < currentStep ? 'bg-green-600' : 'bg-[var(--bg-quaternary)]'
|
||||
}`} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-2">
|
||||
Paso {currentStep}: {steps[currentStep - 1].title}
|
||||
</h2>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
{steps[currentStep - 1].description}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 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;
|
||||
@@ -0,0 +1,499 @@
|
||||
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
frontend/src/pages/app/onboarding/setup/index.ts
Normal file
1
frontend/src/pages/app/onboarding/setup/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as OnboardingSetupPage } from './OnboardingSetupPage';
|
||||
@@ -0,0 +1,438 @@
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingUploadPage;
|
||||
@@ -0,0 +1,438 @@
|
||||
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
frontend/src/pages/app/onboarding/upload/index.ts
Normal file
1
frontend/src/pages/app/onboarding/upload/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as OnboardingUploadPage } from './OnboardingUploadPage';
|
||||
Reference in New Issue
Block a user