Add onboarding flow improvements
This commit is contained in:
@@ -101,7 +101,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
} catch (error) {
|
||||
console.error('Error calling onSuccess:', error);
|
||||
// Fallback: direct redirect if callback fails
|
||||
window.location.href = '/app/onboarding/setup';
|
||||
window.location.href = '/app/onboarding';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Input, Select, Badge } from '../../ui';
|
||||
import type { OnboardingStepProps } from './OnboardingWizard';
|
||||
|
||||
interface CompanyInfo {
|
||||
name: string;
|
||||
type: 'artisan' | 'dependent';
|
||||
size: 'small' | 'medium' | 'large';
|
||||
locations: number;
|
||||
specialties: string[];
|
||||
address: {
|
||||
street: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postal_code: string;
|
||||
country: string;
|
||||
};
|
||||
contact: {
|
||||
phone: string;
|
||||
email: string;
|
||||
website?: string;
|
||||
};
|
||||
established_year?: number;
|
||||
tax_id?: string;
|
||||
}
|
||||
|
||||
const BAKERY_TYPES = [
|
||||
{ value: 'artisan', label: 'Panadería Artesanal Local', description: 'Producción propia y tradicional en el local' },
|
||||
{ value: 'dependent', label: 'Panadería Dependiente', description: 'Dependiente de un panadero central' },
|
||||
];
|
||||
|
||||
const BAKERY_SIZES = [
|
||||
{ value: 'small', label: 'Pequeña', description: '1-10 empleados' },
|
||||
{ value: 'medium', label: 'Mediana', description: '11-50 empleados' },
|
||||
{ value: 'large', label: 'Grande', description: '50+ empleados' },
|
||||
];
|
||||
|
||||
const COMMON_SPECIALTIES = [
|
||||
'Pan tradicional',
|
||||
'Bollería',
|
||||
'Repostería',
|
||||
'Pan integral',
|
||||
'Pasteles',
|
||||
'Productos sin gluten',
|
||||
'Productos veganos',
|
||||
'Pan artesanal',
|
||||
'Productos de temporada',
|
||||
'Catering',
|
||||
];
|
||||
|
||||
export const CompanyInfoStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
}) => {
|
||||
const companyData: CompanyInfo = {
|
||||
name: '',
|
||||
type: 'artisan',
|
||||
size: 'small',
|
||||
locations: 1,
|
||||
specialties: [],
|
||||
address: {
|
||||
street: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postal_code: '',
|
||||
country: 'España',
|
||||
},
|
||||
contact: {
|
||||
phone: '',
|
||||
email: '',
|
||||
website: '',
|
||||
},
|
||||
...data,
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: any) => {
|
||||
onDataChange({
|
||||
...companyData,
|
||||
[field]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddressChange = (field: string, value: string) => {
|
||||
onDataChange({
|
||||
...companyData,
|
||||
address: {
|
||||
...companyData.address,
|
||||
[field]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleContactChange = (field: string, value: string) => {
|
||||
onDataChange({
|
||||
...companyData,
|
||||
contact: {
|
||||
...companyData.contact,
|
||||
[field]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSpecialtyToggle = (specialty: string) => {
|
||||
const currentSpecialties = companyData.specialties || [];
|
||||
const updatedSpecialties = currentSpecialties.includes(specialty)
|
||||
? currentSpecialties.filter(s => s !== specialty)
|
||||
: [...currentSpecialties, specialty];
|
||||
|
||||
handleInputChange('specialties', updatedSpecialties);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Basic Information */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
||||
Información básica
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Nombre de la panadería *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={companyData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
placeholder="Ej: Panadería San Miguel"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Tipo de panadería *
|
||||
</label>
|
||||
<Select
|
||||
value={companyData.type}
|
||||
onChange={(e) => handleInputChange('type', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
{BAKERY_TYPES.map((type) => (
|
||||
<option key={type.value} value={type.value}>
|
||||
{type.label} - {type.description}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Tamaño de la empresa *
|
||||
</label>
|
||||
<Select
|
||||
value={companyData.size}
|
||||
onChange={(e) => handleInputChange('size', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
{BAKERY_SIZES.map((size) => (
|
||||
<option key={size.value} value={size.value}>
|
||||
{size.label} - {size.description}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Número de ubicaciones
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={companyData.locations}
|
||||
onChange={(e) => handleInputChange('locations', parseInt(e.target.value) || 1)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Año de fundación
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1900"
|
||||
max={new Date().getFullYear()}
|
||||
value={companyData.established_year || ''}
|
||||
onChange={(e) => handleInputChange('established_year', parseInt(e.target.value) || undefined)}
|
||||
placeholder="Ej: 1995"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
NIF/CIF
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={companyData.tax_id || ''}
|
||||
onChange={(e) => handleInputChange('tax_id', e.target.value)}
|
||||
placeholder="Ej: B12345678"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Specialties */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
||||
Especialidades
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Selecciona los productos que produces habitualmente
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{COMMON_SPECIALTIES.map((specialty) => (
|
||||
<button
|
||||
key={specialty}
|
||||
onClick={() => handleSpecialtyToggle(specialty)}
|
||||
className={`p-3 text-left border rounded-lg transition-colors ${
|
||||
companyData.specialties?.includes(specialty)
|
||||
? 'border-blue-500 bg-[var(--color-info)]/5 text-[var(--color-info)]'
|
||||
: 'border-[var(--border-primary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm font-medium">{specialty}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{companyData.specialties && companyData.specialties.length > 0 && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Especialidades seleccionadas:
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{companyData.specialties.map((specialty) => (
|
||||
<Badge key={specialty} className="bg-[var(--color-info)]/10 text-[var(--color-info)]">
|
||||
{specialty}
|
||||
<button
|
||||
onClick={() => handleSpecialtyToggle(specialty)}
|
||||
className="ml-1 text-[var(--color-info)] hover:text-[var(--color-info)]"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
||||
Dirección principal
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Dirección *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={companyData.address.street}
|
||||
onChange={(e) => handleAddressChange('street', e.target.value)}
|
||||
placeholder="Calle, número, piso, puerta"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Ciudad *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={companyData.address.city}
|
||||
onChange={(e) => handleAddressChange('city', e.target.value)}
|
||||
placeholder="Ej: Madrid"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Provincia/Estado *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={companyData.address.state}
|
||||
onChange={(e) => handleAddressChange('state', e.target.value)}
|
||||
placeholder="Ej: Madrid"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Código postal *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={companyData.address.postal_code}
|
||||
onChange={(e) => handleAddressChange('postal_code', e.target.value)}
|
||||
placeholder="Ej: 28001"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
País *
|
||||
</label>
|
||||
<Select
|
||||
value={companyData.address.country}
|
||||
onChange={(e) => handleAddressChange('country', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<option value="España">España</option>
|
||||
<option value="Francia">Francia</option>
|
||||
<option value="Portugal">Portugal</option>
|
||||
<option value="Italia">Italia</option>
|
||||
<option value="México">México</option>
|
||||
<option value="Argentina">Argentina</option>
|
||||
<option value="Colombia">Colombia</option>
|
||||
<option value="Otro">Otro</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Information */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
||||
Información de contacto
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Teléfono *
|
||||
</label>
|
||||
<Input
|
||||
type="tel"
|
||||
value={companyData.contact.phone}
|
||||
onChange={(e) => handleContactChange('phone', e.target.value)}
|
||||
placeholder="Ej: +34 911 234 567"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Email de contacto *
|
||||
</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={companyData.contact.email}
|
||||
onChange={(e) => handleContactChange('email', e.target.value)}
|
||||
placeholder="contacto@panaderia.com"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Sitio web (opcional)
|
||||
</label>
|
||||
<Input
|
||||
type="url"
|
||||
value={companyData.contact.website || ''}
|
||||
onChange={(e) => handleContactChange('website', e.target.value)}
|
||||
placeholder="https://www.panaderia.com"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="bg-[var(--bg-secondary)] p-4 rounded-lg">
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Resumen</h4>
|
||||
<div className="text-sm text-[var(--text-secondary)] space-y-1">
|
||||
<p><strong>Panadería:</strong> {companyData.name || 'Sin especificar'}</p>
|
||||
<p><strong>Tipo:</strong> {BAKERY_TYPES.find(t => t.value === companyData.type)?.label}</p>
|
||||
<p><strong>Tamaño:</strong> {BAKERY_SIZES.find(s => s.value === companyData.size)?.label}</p>
|
||||
<p><strong>Especialidades:</strong> {companyData.specialties?.length || 0} seleccionadas</p>
|
||||
<p><strong>Ubicaciones:</strong> {companyData.locations}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompanyInfoStep;
|
||||
@@ -74,7 +74,67 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
|
||||
return true;
|
||||
}, [currentStep, stepData]);
|
||||
|
||||
const goToNextStep = useCallback(() => {
|
||||
const goToNextStep = useCallback(async () => {
|
||||
// Special handling for setup step - create tenant if needed
|
||||
if (currentStep.id === 'setup') {
|
||||
const data = stepData[currentStep.id] || {};
|
||||
|
||||
if (!data.bakery?.tenant_id) {
|
||||
// Create tenant inline
|
||||
updateStepData(currentStep.id, {
|
||||
...data,
|
||||
bakery: { ...data.bakery, isCreating: true }
|
||||
});
|
||||
|
||||
try {
|
||||
const mockTenantService = {
|
||||
createTenant: async (formData: any) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return {
|
||||
tenant_id: `tenant_${Date.now()}`,
|
||||
name: formData.name,
|
||||
...formData
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const tenantData = await mockTenantService.createTenant(data.bakery);
|
||||
|
||||
updateStepData(currentStep.id, {
|
||||
...data,
|
||||
bakery: {
|
||||
...data.bakery,
|
||||
tenant_id: tenantData.tenant_id,
|
||||
created_at: new Date().toISOString(),
|
||||
isCreating: false
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating tenant:', error);
|
||||
updateStepData(currentStep.id, {
|
||||
...data,
|
||||
bakery: { ...data.bakery, isCreating: false }
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for suppliers step - trigger ML training
|
||||
if (currentStep.id === 'suppliers') {
|
||||
const nextStepIndex = currentStepIndex + 1;
|
||||
const nextStep = steps[nextStepIndex];
|
||||
|
||||
if (nextStep && nextStep.id === 'ml-training') {
|
||||
// Set autoStartTraining flag for the ML training step
|
||||
updateStepData(nextStep.id, {
|
||||
...stepData[nextStep.id],
|
||||
autoStartTraining: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (validateCurrentStep()) {
|
||||
if (currentStepIndex < steps.length - 1) {
|
||||
setCurrentStepIndex(currentStepIndex + 1);
|
||||
@@ -83,7 +143,7 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
|
||||
onComplete(stepData);
|
||||
}
|
||||
}
|
||||
}, [currentStepIndex, steps.length, validateCurrentStep, onComplete, stepData]);
|
||||
}, [currentStep.id, currentStepIndex, steps, validateCurrentStep, onComplete, stepData, updateStepData]);
|
||||
|
||||
const goToPreviousStep = useCallback(() => {
|
||||
if (currentStepIndex > 0) {
|
||||
@@ -100,72 +160,186 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
|
||||
};
|
||||
|
||||
const renderStepIndicator = () => (
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = completedSteps.has(step.id);
|
||||
const isCurrent = index === currentStepIndex;
|
||||
const hasError = validationErrors[step.id];
|
||||
<div className="mb-8" role="navigation" aria-label="Progreso del proceso de configuración">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
|
||||
{/* Progress summary */}
|
||||
<div className="text-center mb-6">
|
||||
<div className="inline-flex items-center space-x-3 bg-[var(--bg-secondary)]/50 rounded-full px-6 py-3 backdrop-blur-sm">
|
||||
<span className="text-sm text-[var(--text-secondary)]">Paso</span>
|
||||
<span className="font-bold text-[var(--color-primary)] text-lg">
|
||||
{currentStepIndex + 1}
|
||||
</span>
|
||||
<span className="text-sm text-[var(--text-secondary)]">de</span>
|
||||
<span className="font-bold text-[var(--text-primary)] text-lg">
|
||||
{steps.length}
|
||||
</span>
|
||||
<div className="w-px h-6 bg-[var(--border-secondary)]" />
|
||||
<span className="text-sm font-medium text-[var(--color-primary)]">
|
||||
{Math.round(calculateProgress())}% completado
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className="flex items-center flex-1"
|
||||
>
|
||||
<button
|
||||
onClick={() => goToStep(index)}
|
||||
disabled={index > currentStepIndex + 1}
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium transition-colors ${
|
||||
isCompleted
|
||||
? 'bg-green-500 text-white'
|
||||
: isCurrent
|
||||
? hasError
|
||||
? 'bg-red-500 text-white'
|
||||
: 'bg-[var(--color-info)]/50 text-white'
|
||||
: 'bg-[var(--bg-quaternary)] text-[var(--text-secondary)] hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{isCompleted ? '✓' : index + 1}
|
||||
</button>
|
||||
|
||||
<div className="ml-3 flex-1">
|
||||
<p className={`text-sm font-medium ${isCurrent ? 'text-[var(--color-info)]' : 'text-[var(--text-secondary)]'}`}>
|
||||
{step.title}
|
||||
{step.isRequired && <span className="text-red-500 ml-1">*</span>}
|
||||
</p>
|
||||
{hasError && (
|
||||
<p className="text-xs text-[var(--color-error)] mt-1">{hasError}</p>
|
||||
)}
|
||||
{/* Progress connections between steps */}
|
||||
<div className="relative mb-8" role="progressbar"
|
||||
aria-valuenow={completedSteps.size}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={steps.length}
|
||||
aria-label={`${completedSteps.size} de ${steps.length} pasos completados`}>
|
||||
|
||||
{/* Step circles */}
|
||||
<div className="relative">
|
||||
{/* Desktop view */}
|
||||
<div className="hidden md:flex justify-between items-center">
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = completedSteps.has(step.id);
|
||||
const isCurrent = index === currentStepIndex;
|
||||
const hasError = validationErrors[step.id];
|
||||
const isAccessible = index <= currentStepIndex + 1;
|
||||
const nextStepCompleted = index < steps.length - 1 && completedSteps.has(steps[index + 1].id);
|
||||
|
||||
return (
|
||||
<React.Fragment key={step.id}>
|
||||
<div className="flex flex-col items-center">
|
||||
<button
|
||||
onClick={() => isAccessible ? goToStep(index) : null}
|
||||
disabled={!isAccessible}
|
||||
className={`
|
||||
relative flex items-center justify-center w-12 h-12 rounded-full text-sm font-bold z-10
|
||||
transition-all duration-300 transform hover:scale-110 focus:outline-none focus:ring-4 focus:ring-opacity-50
|
||||
${
|
||||
isCompleted
|
||||
? 'bg-[var(--color-success)] text-white shadow-lg hover:bg-[var(--color-success)]/90 focus:ring-[var(--color-success)]'
|
||||
: isCurrent
|
||||
? hasError
|
||||
? 'bg-[var(--color-error)] text-white shadow-lg animate-pulse focus:ring-[var(--color-error)]'
|
||||
: 'bg-[var(--color-primary)] text-white shadow-xl scale-110 ring-4 ring-[var(--color-primary)]/20 focus:ring-[var(--color-primary)]'
|
||||
: isAccessible
|
||||
? 'bg-[var(--bg-primary)] border-2 border-[var(--border-secondary)] text-[var(--text-secondary)] shadow-sm hover:border-[var(--color-primary)]/50 hover:shadow-md focus:ring-[var(--color-primary)]'
|
||||
: 'bg-[var(--bg-secondary)] border-2 border-[var(--border-tertiary)] text-[var(--text-tertiary)] cursor-not-allowed opacity-50'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<span>{index + 1}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Step label */}
|
||||
<div className={`mt-3 text-center max-w-20 ${
|
||||
isCurrent
|
||||
? 'text-[var(--color-primary)] font-semibold'
|
||||
: isCompleted
|
||||
? 'text-[var(--color-success)] font-medium'
|
||||
: 'text-[var(--text-secondary)]'
|
||||
}`}>
|
||||
<div className="text-xs leading-tight">
|
||||
{step.title.replace(/🏢|📊|🤖|📋|⚙️|🏪|🧠|🎉/g, '').trim()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error indicator */}
|
||||
{hasError && (
|
||||
<div className="absolute -top-1 -right-1 w-4 h-4 bg-[var(--color-error)] rounded-full flex items-center justify-center z-20">
|
||||
<span className="text-white text-xs">!</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Connection line to next step */}
|
||||
{index < steps.length - 1 && (
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="h-1 w-full mx-4 rounded-full transition-all duration-500">
|
||||
<div className={`h-full rounded-full ${
|
||||
isCompleted ? 'bg-[var(--color-success)]' : 'bg-[var(--bg-secondary)]'
|
||||
}`} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{index < steps.length - 1 && (
|
||||
<div className={`flex-1 h-0.5 mx-4 ${
|
||||
isCompleted ? 'bg-green-500' : 'bg-[var(--bg-quaternary)]'
|
||||
}`} />
|
||||
)}
|
||||
{/* Mobile view - simplified horizontal scroll */}
|
||||
<div className="md:hidden">
|
||||
<div className="flex items-center overflow-x-auto pb-4 px-4">
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = completedSteps.has(step.id);
|
||||
const isCurrent = index === currentStepIndex;
|
||||
const hasError = validationErrors[step.id];
|
||||
const isAccessible = index <= currentStepIndex + 1;
|
||||
|
||||
return (
|
||||
<React.Fragment key={step.id}>
|
||||
<div className="flex-shrink-0 flex flex-col items-center">
|
||||
<button
|
||||
onClick={() => isAccessible ? goToStep(index) : null}
|
||||
disabled={!isAccessible}
|
||||
className={`
|
||||
relative flex items-center justify-center w-10 h-10 rounded-full text-xs font-bold z-10
|
||||
transition-all duration-300 focus:outline-none focus:ring-3 focus:ring-opacity-50
|
||||
${
|
||||
isCompleted
|
||||
? 'bg-[var(--color-success)] text-white shadow-lg focus:ring-[var(--color-success)]'
|
||||
: isCurrent
|
||||
? hasError
|
||||
? 'bg-[var(--color-error)] text-white shadow-lg focus:ring-[var(--color-error)]'
|
||||
: 'bg-[var(--color-primary)] text-white shadow-lg ring-3 ring-[var(--color-primary)]/20 focus:ring-[var(--color-primary)]'
|
||||
: isAccessible
|
||||
? 'bg-[var(--bg-primary)] border-2 border-[var(--border-secondary)] text-[var(--text-secondary)] focus:ring-[var(--color-primary)]'
|
||||
: 'bg-[var(--bg-secondary)] border-2 border-[var(--border-tertiary)] text-[var(--text-tertiary)] cursor-not-allowed opacity-50'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<span>{index + 1}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Step label for mobile */}
|
||||
<div className={`mt-2 text-center min-w-16 ${
|
||||
isCurrent
|
||||
? 'text-[var(--color-primary)] font-semibold'
|
||||
: isCompleted
|
||||
? 'text-[var(--color-success)] font-medium'
|
||||
: 'text-[var(--text-secondary)]'
|
||||
}`}>
|
||||
<div className="text-xs leading-tight">
|
||||
{step.title.replace(/🏢|📊|🤖|📋|⚙️|🏪|🧠|🎉/g, '').trim()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connection line for mobile */}
|
||||
{index < steps.length - 1 && (
|
||||
<div className="flex-shrink-0 w-8 h-1 mx-2 rounded-full transition-all duration-500">
|
||||
<div className={`w-full h-full rounded-full ${
|
||||
isCompleted ? 'bg-[var(--color-success)]' : 'bg-[var(--bg-secondary)]'
|
||||
}`} />
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderProgressBar = () => (
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Progreso del onboarding
|
||||
</span>
|
||||
<span className="text-sm text-[var(--text-tertiary)]">
|
||||
{completedSteps.size} de {steps.length} completados
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
|
||||
<div
|
||||
className="bg-[var(--color-info)]/50 h-2 rounded-full transition-all duration-500"
|
||||
style={{ width: `${calculateProgress()}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!currentStep) {
|
||||
return (
|
||||
@@ -178,75 +352,92 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
|
||||
const StepComponent = currentStep.component;
|
||||
|
||||
return (
|
||||
<div className={`max-w-4xl mx-auto ${className}`}>
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
<div className={`min-h-screen bg-[var(--bg-primary)] ${className}`}>
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
{/* Header with clean design */}
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-3xl font-bold text-[var(--text-primary)] mb-2">
|
||||
Configuración inicial
|
||||
</h1>
|
||||
<p className="text-[var(--text-secondary)] mt-1">
|
||||
<p className="text-[var(--text-secondary)] mb-4">
|
||||
Completa estos pasos para comenzar a usar la plataforma
|
||||
</p>
|
||||
|
||||
{onExit && (
|
||||
<button
|
||||
onClick={onExit}
|
||||
className="absolute top-8 right-8 text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] text-2xl"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{onExit && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onExit}
|
||||
className="text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]"
|
||||
>
|
||||
✕ Salir
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{renderStepIndicator()}
|
||||
|
||||
{renderProgressBar()}
|
||||
{renderStepIndicator()}
|
||||
{/* Main Content - Single clean card */}
|
||||
<div className="bg-[var(--bg-primary)] border border-[var(--border-primary)] rounded-2xl shadow-xl overflow-hidden">
|
||||
{/* Step header */}
|
||||
<div className="bg-[var(--bg-secondary)] px-8 py-6 border-b border-[var(--border-secondary)]">
|
||||
<h2 className="text-2xl font-semibold text-[var(--text-primary)] mb-2">
|
||||
{currentStep.title}
|
||||
</h2>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
{currentStep.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Current Step Content */}
|
||||
<Card className="p-6">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-2">
|
||||
{currentStep.title}
|
||||
</h2>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
{currentStep.description}
|
||||
</p>
|
||||
{/* Step content */}
|
||||
<div className="px-8 py-8">
|
||||
<StepComponent
|
||||
data={{
|
||||
...stepData[currentStep.id],
|
||||
// Pass all step data to allow access to previous steps
|
||||
allStepData: stepData
|
||||
}}
|
||||
onDataChange={(data) => updateStepData(currentStep.id, data)}
|
||||
onNext={goToNextStep}
|
||||
onPrevious={goToPreviousStep}
|
||||
isFirstStep={currentStepIndex === 0}
|
||||
isLastStep={currentStepIndex === steps.length - 1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Navigation footer */}
|
||||
<div className="bg-[var(--bg-secondary)] px-8 py-6 border-t border-[var(--border-secondary)]">
|
||||
<div className="flex justify-between items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={goToPreviousStep}
|
||||
disabled={currentStepIndex === 0}
|
||||
className="px-6"
|
||||
>
|
||||
← Anterior
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={goToNextStep}
|
||||
disabled={
|
||||
(currentStep.validation && currentStep.validation(stepData[currentStep.id] || {})) ||
|
||||
(currentStep.id === 'setup' && stepData[currentStep.id]?.bakery?.isCreating)
|
||||
}
|
||||
className="px-8"
|
||||
>
|
||||
{currentStep.id === 'setup' && stepData[currentStep.id]?.bakery?.isCreating ? (
|
||||
<>
|
||||
<div className="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2" />
|
||||
Creando espacio...
|
||||
</>
|
||||
) : currentStep.id === 'suppliers' ? (
|
||||
'Siguiente →'
|
||||
) : (
|
||||
currentStepIndex === steps.length - 1 ? '🎉 Finalizar' : 'Siguiente →'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StepComponent
|
||||
data={stepData[currentStep.id] || {}}
|
||||
onDataChange={(data) => updateStepData(currentStep.id, data)}
|
||||
onNext={goToNextStep}
|
||||
onPrevious={goToPreviousStep}
|
||||
isFirstStep={currentStepIndex === 0}
|
||||
isLastStep={currentStepIndex === steps.length - 1}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between items-center mt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={goToPreviousStep}
|
||||
disabled={currentStepIndex === 0}
|
||||
>
|
||||
← Anterior
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-[var(--text-tertiary)]">
|
||||
Paso {currentStepIndex + 1} de {steps.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={goToNextStep}
|
||||
>
|
||||
{currentStepIndex === steps.length - 1 ? 'Finalizar' : 'Siguiente →'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,556 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Card, Button, Input, Select, Badge } from '../../ui';
|
||||
import type { OnboardingStepProps } from './OnboardingWizard';
|
||||
|
||||
interface SystemConfig {
|
||||
timezone: string;
|
||||
currency: string;
|
||||
language: string;
|
||||
date_format: string;
|
||||
number_format: string;
|
||||
working_hours: {
|
||||
start: string;
|
||||
end: string;
|
||||
days: number[];
|
||||
};
|
||||
notifications: {
|
||||
email_enabled: boolean;
|
||||
sms_enabled: boolean;
|
||||
push_enabled: boolean;
|
||||
alert_preferences: string[];
|
||||
};
|
||||
integrations: {
|
||||
pos_system?: string;
|
||||
accounting_software?: string;
|
||||
payment_provider?: string;
|
||||
};
|
||||
features: {
|
||||
inventory_management: boolean;
|
||||
production_planning: boolean;
|
||||
sales_analytics: boolean;
|
||||
customer_management: boolean;
|
||||
financial_reporting: boolean;
|
||||
quality_control: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const TIMEZONES = [
|
||||
{ value: 'Europe/Madrid', label: 'Madrid (GMT+1)' },
|
||||
{ value: 'Europe/London', label: 'London (GMT+0)' },
|
||||
{ value: 'Europe/Paris', label: 'Paris (GMT+1)' },
|
||||
{ value: 'America/New_York', label: 'New York (GMT-5)' },
|
||||
{ value: 'America/Los_Angeles', label: 'Los Angeles (GMT-8)' },
|
||||
{ value: 'America/Mexico_City', label: 'Mexico City (GMT-6)' },
|
||||
{ value: 'America/Buenos_Aires', label: 'Buenos Aires (GMT-3)' },
|
||||
];
|
||||
|
||||
const CURRENCIES = [
|
||||
{ value: 'EUR', label: 'Euro (€)', symbol: '€' },
|
||||
{ value: 'USD', label: 'US Dollar ($)', symbol: '$' },
|
||||
{ value: 'GBP', label: 'British Pound (£)', symbol: '£' },
|
||||
{ value: 'MXN', label: 'Mexican Peso ($)', symbol: '$' },
|
||||
{ value: 'ARS', label: 'Argentine Peso ($)', symbol: '$' },
|
||||
{ value: 'COP', label: 'Colombian Peso ($)', symbol: '$' },
|
||||
];
|
||||
|
||||
const LANGUAGES = [
|
||||
{ value: 'es', label: 'Español' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'fr', label: 'Français' },
|
||||
{ value: 'pt', label: 'Português' },
|
||||
{ value: 'it', label: 'Italiano' },
|
||||
];
|
||||
|
||||
const DAYS_OF_WEEK = [
|
||||
{ value: 1, label: 'Lunes', short: 'L' },
|
||||
{ value: 2, label: 'Martes', short: 'M' },
|
||||
{ value: 3, label: 'Miércoles', short: 'X' },
|
||||
{ value: 4, label: 'Jueves', short: 'J' },
|
||||
{ value: 5, label: 'Viernes', short: 'V' },
|
||||
{ value: 6, label: 'Sábado', short: 'S' },
|
||||
{ value: 0, label: 'Domingo', short: 'D' },
|
||||
];
|
||||
|
||||
const ALERT_TYPES = [
|
||||
{ value: 'low_stock', label: 'Stock bajo', description: 'Cuando los ingredientes están por debajo del mínimo' },
|
||||
{ value: 'production_delays', label: 'Retrasos de producción', description: 'Cuando los lotes se retrasan' },
|
||||
{ value: 'quality_issues', label: 'Problemas de calidad', description: 'Cuando se detectan problemas de calidad' },
|
||||
{ value: 'financial_targets', label: 'Objetivos financieros', description: 'Cuando se alcanzan o no se cumplen objetivos' },
|
||||
{ value: 'equipment_maintenance', label: 'Mantenimiento de equipos', description: 'Recordatorios de mantenimiento' },
|
||||
{ value: 'food_safety', label: 'Seguridad alimentaria', description: 'Alertas relacionadas con seguridad alimentaria' },
|
||||
];
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
key: 'inventory_management',
|
||||
title: 'Gestión de inventario',
|
||||
description: 'Control de stock, ingredientes y materias primas',
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
key: 'production_planning',
|
||||
title: 'Planificación de producción',
|
||||
description: 'Programación de lotes y gestión de recetas',
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
key: 'sales_analytics',
|
||||
title: 'Analytics de ventas',
|
||||
description: 'Reportes y análisis de ventas y tendencias',
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
key: 'customer_management',
|
||||
title: 'Gestión de clientes',
|
||||
description: 'Base de datos de clientes y programa de fidelización',
|
||||
recommended: false,
|
||||
},
|
||||
{
|
||||
key: 'financial_reporting',
|
||||
title: 'Reportes financieros',
|
||||
description: 'Análisis de costos, márgenes y rentabilidad',
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
key: 'quality_control',
|
||||
title: 'Control de calidad',
|
||||
description: 'Seguimiento de calidad y estándares',
|
||||
recommended: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const SystemSetupStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
}) => {
|
||||
const systemData: SystemConfig = {
|
||||
timezone: 'Europe/Madrid',
|
||||
currency: 'EUR',
|
||||
language: 'es',
|
||||
date_format: 'DD/MM/YYYY',
|
||||
number_format: 'European',
|
||||
working_hours: {
|
||||
start: '06:00',
|
||||
end: '20:00',
|
||||
days: [1, 2, 3, 4, 5, 6], // Monday to Saturday
|
||||
},
|
||||
notifications: {
|
||||
email_enabled: true,
|
||||
sms_enabled: false,
|
||||
push_enabled: true,
|
||||
alert_preferences: ['low_stock', 'production_delays', 'quality_issues'],
|
||||
},
|
||||
integrations: {},
|
||||
features: {
|
||||
inventory_management: true,
|
||||
production_planning: true,
|
||||
sales_analytics: true,
|
||||
customer_management: false,
|
||||
financial_reporting: true,
|
||||
quality_control: true,
|
||||
},
|
||||
...data,
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: any) => {
|
||||
onDataChange({
|
||||
...systemData,
|
||||
[field]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleWorkingHoursChange = (field: string, value: any) => {
|
||||
onDataChange({
|
||||
...systemData,
|
||||
working_hours: {
|
||||
...systemData.working_hours,
|
||||
[field]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleNotificationChange = (field: string, value: any) => {
|
||||
onDataChange({
|
||||
...systemData,
|
||||
notifications: {
|
||||
...systemData.notifications,
|
||||
[field]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleIntegrationChange = (field: string, value: string) => {
|
||||
onDataChange({
|
||||
...systemData,
|
||||
integrations: {
|
||||
...systemData.integrations,
|
||||
[field]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleFeatureToggle = (feature: string) => {
|
||||
onDataChange({
|
||||
...systemData,
|
||||
features: {
|
||||
...systemData.features,
|
||||
[feature]: !systemData.features[feature as keyof typeof systemData.features],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const toggleWorkingDay = (day: number) => {
|
||||
const currentDays = systemData.working_hours.days;
|
||||
const updatedDays = currentDays.includes(day)
|
||||
? currentDays.filter(d => d !== day)
|
||||
: [...currentDays, day].sort();
|
||||
|
||||
handleWorkingHoursChange('days', updatedDays);
|
||||
};
|
||||
|
||||
const toggleAlertPreference = (alert: string) => {
|
||||
const current = systemData.notifications.alert_preferences;
|
||||
const updated = current.includes(alert)
|
||||
? current.filter(a => a !== alert)
|
||||
: [...current, alert];
|
||||
|
||||
handleNotificationChange('alert_preferences', updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Regional Settings */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
||||
Configuración regional
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Zona horaria *
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.timezone}
|
||||
onChange={(e) => handleInputChange('timezone', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
{TIMEZONES.map((tz) => (
|
||||
<option key={tz.value} value={tz.value}>
|
||||
{tz.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Moneda *
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.currency}
|
||||
onChange={(e) => handleInputChange('currency', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
{CURRENCIES.map((currency) => (
|
||||
<option key={currency.value} value={currency.value}>
|
||||
{currency.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Idioma *
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.language}
|
||||
onChange={(e) => handleInputChange('language', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
{LANGUAGES.map((lang) => (
|
||||
<option key={lang.value} value={lang.value}>
|
||||
{lang.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Formato de fecha *
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.date_format}
|
||||
onChange={(e) => handleInputChange('date_format', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<option value="DD/MM/YYYY">DD/MM/YYYY</option>
|
||||
<option value="MM/DD/YYYY">MM/DD/YYYY</option>
|
||||
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Working Hours */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
||||
Horario de trabajo
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Hora de apertura *
|
||||
</label>
|
||||
<Input
|
||||
type="time"
|
||||
value={systemData.working_hours.start}
|
||||
onChange={(e) => handleWorkingHoursChange('start', e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Hora de cierre *
|
||||
</label>
|
||||
<Input
|
||||
type="time"
|
||||
value={systemData.working_hours.end}
|
||||
onChange={(e) => handleWorkingHoursChange('end', e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Días de operación *
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{DAYS_OF_WEEK.map((day) => (
|
||||
<button
|
||||
key={day.value}
|
||||
onClick={() => toggleWorkingDay(day.value)}
|
||||
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
systemData.working_hours.days.includes(day.value)
|
||||
? 'bg-[var(--color-info)]/50 text-white'
|
||||
: 'bg-[var(--bg-quaternary)] text-[var(--text-secondary)] hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{day.short} - {day.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
||||
Módulos y características
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Selecciona las características que quieres activar. Podrás cambiar esto más tarde.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{FEATURES.map((feature) => (
|
||||
<Card
|
||||
key={feature.key}
|
||||
className={`p-4 cursor-pointer transition-colors ${
|
||||
systemData.features[feature.key as keyof typeof systemData.features]
|
||||
? 'border-blue-500 bg-[var(--color-info)]/5'
|
||||
: 'border-[var(--border-primary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
onClick={() => handleFeatureToggle(feature.key)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-medium text-[var(--text-primary)]">{feature.title}</h4>
|
||||
{feature.recommended && (
|
||||
<Badge className="bg-[var(--color-success)]/10 text-[var(--color-success)] text-xs">
|
||||
Recomendado
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{feature.description}</p>
|
||||
</div>
|
||||
|
||||
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
|
||||
systemData.features[feature.key as keyof typeof systemData.features]
|
||||
? 'border-blue-500 bg-[var(--color-info)]/50'
|
||||
: 'border-[var(--border-secondary)]'
|
||||
}`}>
|
||||
{systemData.features[feature.key as keyof typeof systemData.features] && (
|
||||
<span className="text-white text-xs">✓</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
||||
Notificaciones
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={systemData.notifications.email_enabled}
|
||||
onChange={(e) => handleNotificationChange('email_enabled', e.target.checked)}
|
||||
className="mr-2 rounded"
|
||||
/>
|
||||
Notificaciones por email
|
||||
</label>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={systemData.notifications.push_enabled}
|
||||
onChange={(e) => handleNotificationChange('push_enabled', e.target.checked)}
|
||||
className="mr-2 rounded"
|
||||
/>
|
||||
Notificaciones push
|
||||
</label>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={systemData.notifications.sms_enabled}
|
||||
onChange={(e) => handleNotificationChange('sms_enabled', e.target.checked)}
|
||||
className="mr-2 rounded"
|
||||
/>
|
||||
Notificaciones por SMS
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Tipos de alertas que deseas recibir
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{ALERT_TYPES.map((alert) => (
|
||||
<div
|
||||
key={alert.value}
|
||||
className={`p-3 border rounded-lg cursor-pointer transition-colors ${
|
||||
systemData.notifications.alert_preferences.includes(alert.value)
|
||||
? 'border-blue-500 bg-[var(--color-info)]/5'
|
||||
: 'border-[var(--border-primary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
onClick={() => toggleAlertPreference(alert.value)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h5 className="font-medium text-[var(--text-primary)]">{alert.label}</h5>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{alert.description}</p>
|
||||
</div>
|
||||
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
|
||||
systemData.notifications.alert_preferences.includes(alert.value)
|
||||
? 'border-blue-500 bg-[var(--color-info)]/50'
|
||||
: 'border-[var(--border-secondary)]'
|
||||
}`}>
|
||||
{systemData.notifications.alert_preferences.includes(alert.value) && (
|
||||
<span className="text-white text-xs">✓</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Integrations */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
||||
Integraciones (opcional)
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Estas integraciones se pueden configurar más tarde desde el panel de administración.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Sistema POS
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.integrations.pos_system || ''}
|
||||
onChange={(e) => handleIntegrationChange('pos_system', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<option value="">Seleccionar...</option>
|
||||
<option value="square">Square</option>
|
||||
<option value="shopify">Shopify POS</option>
|
||||
<option value="toast">Toast</option>
|
||||
<option value="lightspeed">Lightspeed</option>
|
||||
<option value="other">Otro</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Software de contabilidad
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.integrations.accounting_software || ''}
|
||||
onChange={(e) => handleIntegrationChange('accounting_software', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<option value="">Seleccionar...</option>
|
||||
<option value="sage">Sage</option>
|
||||
<option value="contaplus">ContaPlus</option>
|
||||
<option value="a3">A3 Software</option>
|
||||
<option value="quickbooks">QuickBooks</option>
|
||||
<option value="other">Otro</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
||||
Proveedor de pagos
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.integrations.payment_provider || ''}
|
||||
onChange={(e) => handleIntegrationChange('payment_provider', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<option value="">Seleccionar...</option>
|
||||
<option value="stripe">Stripe</option>
|
||||
<option value="paypal">PayPal</option>
|
||||
<option value="redsys">Redsys</option>
|
||||
<option value="bizum">Bizum</option>
|
||||
<option value="other">Otro</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="bg-[var(--bg-secondary)] p-4 rounded-lg">
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Configuración seleccionada</h4>
|
||||
<div className="text-sm text-[var(--text-secondary)] space-y-1">
|
||||
<p><strong>Zona horaria:</strong> {TIMEZONES.find(tz => tz.value === systemData.timezone)?.label}</p>
|
||||
<p><strong>Moneda:</strong> {CURRENCIES.find(c => c.value === systemData.currency)?.label}</p>
|
||||
<p><strong>Horario:</strong> {systemData.working_hours.start} - {systemData.working_hours.end}</p>
|
||||
<p><strong>Días operativos:</strong> {systemData.working_hours.days.length} días por semana</p>
|
||||
<p><strong>Módulos activados:</strong> {Object.values(systemData.features).filter(Boolean).length} de {Object.keys(systemData.features).length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemSetupStep;
|
||||
@@ -1,7 +1,14 @@
|
||||
// Onboarding domain components
|
||||
export { default as OnboardingWizard } from './OnboardingWizard';
|
||||
export { default as CompanyInfoStep } from './CompanyInfoStep';
|
||||
export { default as SystemSetupStep } from './SystemSetupStep';
|
||||
|
||||
// Individual step components
|
||||
export { BakerySetupStep } from './steps/BakerySetupStep';
|
||||
export { DataProcessingStep } from './steps/DataProcessingStep';
|
||||
export { ReviewStep } from './steps/ReviewStep';
|
||||
export { InventorySetupStep } from './steps/InventorySetupStep';
|
||||
export { SuppliersStep } from './steps/SuppliersStep';
|
||||
export { MLTrainingStep } from './steps/MLTrainingStep';
|
||||
export { CompletionStep } from './steps/CompletionStep';
|
||||
|
||||
// Re-export types from wizard
|
||||
export type { OnboardingStep, OnboardingStepProps } from './OnboardingWizard';
|
||||
@@ -0,0 +1,191 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Store, MapPin, Phone, Mail } from 'lucide-react';
|
||||
import { Button, Card, Input } from '../../../ui';
|
||||
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||
|
||||
export const BakerySetupStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
onNext,
|
||||
onPrevious,
|
||||
isFirstStep,
|
||||
isLastStep
|
||||
}) => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: data.bakery?.name || '',
|
||||
type: data.bakery?.type || '',
|
||||
location: data.bakery?.location || '',
|
||||
phone: data.bakery?.phone || '',
|
||||
email: data.bakery?.email || '',
|
||||
description: data.bakery?.description || ''
|
||||
});
|
||||
|
||||
const bakeryTypes = [
|
||||
{
|
||||
value: 'artisan',
|
||||
label: 'Panadería Artesanal',
|
||||
description: 'Producción propia tradicional con recetas artesanales',
|
||||
icon: '🥖'
|
||||
},
|
||||
{
|
||||
value: 'industrial',
|
||||
label: 'Panadería Industrial',
|
||||
description: 'Producción a gran escala con procesos automatizados',
|
||||
icon: '🏭'
|
||||
},
|
||||
{
|
||||
value: 'retail',
|
||||
label: 'Panadería Retail',
|
||||
description: 'Punto de venta que compra productos terminados',
|
||||
icon: '🏪'
|
||||
},
|
||||
{
|
||||
value: 'hybrid',
|
||||
label: 'Modelo Híbrido',
|
||||
description: 'Combina producción propia con productos externos',
|
||||
icon: '🔄'
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Update parent data when form changes
|
||||
onDataChange({
|
||||
bakery: {
|
||||
...formData,
|
||||
tenant_id: data.bakery?.tenant_id
|
||||
}
|
||||
});
|
||||
}, [formData]);
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
|
||||
{/* Form */}
|
||||
<div className="space-y-8">
|
||||
{/* Basic Info */}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-3">
|
||||
Nombre de la Panadería *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
placeholder="Ej: Panadería Artesanal El Buen Pan"
|
||||
className="w-full text-lg py-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bakery Type - Simplified */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-4">
|
||||
Tipo de Panadería *
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{bakeryTypes.map((type) => (
|
||||
<label
|
||||
key={type.value}
|
||||
className={`
|
||||
flex items-center p-4 border-2 rounded-lg cursor-pointer transition-all duration-200 hover:shadow-sm
|
||||
${formData.type === type.value
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="bakeryType"
|
||||
value={type.value}
|
||||
checked={formData.type === type.value}
|
||||
onChange={(e) => handleInputChange('type', e.target.value)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="flex items-center space-x-3 w-full">
|
||||
<span className="text-2xl">{type.icon}</span>
|
||||
<div>
|
||||
<h4 className="font-medium text-[var(--text-primary)]">
|
||||
{type.label}
|
||||
</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{type.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location and Contact - Simplified */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-3">
|
||||
Ubicación *
|
||||
</label>
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-[var(--text-tertiary)]" />
|
||||
<Input
|
||||
value={formData.location}
|
||||
onChange={(e) => handleInputChange('location', e.target.value)}
|
||||
placeholder="Dirección completa de tu panadería"
|
||||
className="w-full pl-12 py-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
Teléfono (opcional)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<Input
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
placeholder="+1 234 567 8900"
|
||||
className="w-full pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
Email (opcional)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<Input
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
placeholder="contacto@panaderia.com"
|
||||
className="w-full pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Optional: Show loading state when creating tenant */}
|
||||
{data.bakery?.isCreating && (
|
||||
<div className="text-center p-6 bg-[var(--color-primary)]/5 rounded-lg">
|
||||
<div className="animate-spin w-8 h-8 border-3 border-[var(--color-primary)] border-t-transparent rounded-full mx-auto mb-4" />
|
||||
<p className="text-[var(--color-primary)] font-medium">
|
||||
Creando tu espacio de trabajo...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,387 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { CheckCircle, Star, Rocket, Gift, Download, Share2, ArrowRight, Calendar } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../ui';
|
||||
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||
|
||||
interface CompletionStats {
|
||||
totalProducts: number;
|
||||
inventoryItems: number;
|
||||
suppliersConfigured: number;
|
||||
mlModelAccuracy: number;
|
||||
estimatedTimeSaved: string;
|
||||
completionScore: number;
|
||||
}
|
||||
|
||||
export const CompletionStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
onNext,
|
||||
onPrevious,
|
||||
isFirstStep,
|
||||
isLastStep
|
||||
}) => {
|
||||
const [showConfetti, setShowConfetti] = useState(false);
|
||||
const [completionStats, setCompletionStats] = useState<CompletionStats | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Show confetti animation
|
||||
setShowConfetti(true);
|
||||
const timer = setTimeout(() => setShowConfetti(false), 3000);
|
||||
|
||||
// Calculate completion stats
|
||||
const stats: CompletionStats = {
|
||||
totalProducts: data.detectedProducts?.filter((p: any) => p.status === 'approved').length || 0,
|
||||
inventoryItems: data.inventoryItems?.length || 0,
|
||||
suppliersConfigured: data.suppliers?.length || 0,
|
||||
mlModelAccuracy: data.trainingMetrics?.accuracy * 100 || 0,
|
||||
estimatedTimeSaved: '15-20 horas',
|
||||
completionScore: calculateCompletionScore()
|
||||
};
|
||||
|
||||
setCompletionStats(stats);
|
||||
|
||||
// Update parent data
|
||||
onDataChange({
|
||||
...data,
|
||||
completionStats: stats,
|
||||
onboardingCompleted: true,
|
||||
completedAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const calculateCompletionScore = () => {
|
||||
let score = 0;
|
||||
|
||||
// Base score for completing setup
|
||||
if (data.bakery?.tenant_id) score += 20;
|
||||
|
||||
// Data upload and analysis
|
||||
if (data.validation?.is_valid) score += 15;
|
||||
if (data.analysisStatus === 'completed') score += 15;
|
||||
|
||||
// Product review
|
||||
const approvedProducts = data.detectedProducts?.filter((p: any) => p.status === 'approved').length || 0;
|
||||
if (approvedProducts > 0) score += 20;
|
||||
|
||||
// Inventory setup
|
||||
if (data.inventoryItems?.length > 0) score += 15;
|
||||
|
||||
// ML training
|
||||
if (data.trainingStatus === 'completed') score += 15;
|
||||
|
||||
return Math.min(score, 100);
|
||||
};
|
||||
|
||||
const generateCertificate = () => {
|
||||
// Mock certificate generation
|
||||
const certificateData = {
|
||||
bakeryName: data.bakery?.name || 'Tu Panadería',
|
||||
completionDate: new Date().toLocaleDateString('es-ES'),
|
||||
score: completionStats?.completionScore || 0,
|
||||
features: [
|
||||
'Configuración de Tenant Multi-inquilino',
|
||||
'Análisis de Datos con IA',
|
||||
'Gestión de Inventario Inteligente',
|
||||
'Entrenamiento de Modelo ML',
|
||||
'Integración de Proveedores'
|
||||
]
|
||||
};
|
||||
|
||||
console.log('Generating certificate:', certificateData);
|
||||
alert(`🎓 Certificado generado para ${certificateData.bakeryName}\nPuntuación: ${certificateData.score}/100`);
|
||||
};
|
||||
|
||||
const scheduleDemo = () => {
|
||||
// Mock demo scheduling
|
||||
alert('📅 Te contactaremos pronto para agendar una demostración personalizada de las funcionalidades avanzadas.');
|
||||
};
|
||||
|
||||
const shareSuccess = () => {
|
||||
// Mock social sharing
|
||||
const shareText = `¡Acabo de completar la configuración de mi panadería inteligente con IA! 🥖🤖 Puntuación: ${completionStats?.completionScore}/100`;
|
||||
navigator.clipboard.writeText(shareText);
|
||||
alert('✅ Texto copiado al portapapeles. ¡Compártelo en tus redes sociales!');
|
||||
};
|
||||
|
||||
const quickStartActions = [
|
||||
{
|
||||
id: 'dashboard',
|
||||
title: 'Ir al Dashboard',
|
||||
description: 'Explora tu panel de control personalizado',
|
||||
icon: <ArrowRight className="w-5 h-5" />,
|
||||
action: () => onNext(),
|
||||
primary: true
|
||||
},
|
||||
{
|
||||
id: 'inventory',
|
||||
title: 'Gestionar Inventario',
|
||||
description: 'Revisa y actualiza tu stock actual',
|
||||
icon: <Gift className="w-5 h-5" />,
|
||||
action: () => console.log('Navigate to inventory'),
|
||||
primary: false
|
||||
},
|
||||
{
|
||||
id: 'predictions',
|
||||
title: 'Ver Predicciones IA',
|
||||
description: 'Consulta las predicciones de demanda',
|
||||
icon: <Rocket className="w-5 h-5" />,
|
||||
action: () => console.log('Navigate to predictions'),
|
||||
primary: false
|
||||
}
|
||||
];
|
||||
|
||||
const achievementBadges = [
|
||||
{
|
||||
title: 'Pionero IA',
|
||||
description: 'Primera configuración con Machine Learning',
|
||||
icon: '🤖',
|
||||
earned: data.trainingStatus === 'completed'
|
||||
},
|
||||
{
|
||||
title: 'Organizador',
|
||||
description: 'Inventario completamente configurado',
|
||||
icon: '📦',
|
||||
earned: (data.inventoryItems?.length || 0) >= 5
|
||||
},
|
||||
{
|
||||
title: 'Analista',
|
||||
description: 'Análisis de datos exitoso',
|
||||
icon: '📊',
|
||||
earned: data.analysisStatus === 'completed'
|
||||
},
|
||||
{
|
||||
title: 'Perfeccionista',
|
||||
description: 'Puntuación de configuración 90+',
|
||||
icon: '⭐',
|
||||
earned: (completionStats?.completionScore || 0) >= 90
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Confetti Effect */}
|
||||
{showConfetti && (
|
||||
<div className="fixed inset-0 pointer-events-none z-50 overflow-hidden">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-6xl animate-bounce">🎉</div>
|
||||
</div>
|
||||
{/* Additional confetti elements could be added here */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Celebration Header */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-24 h-24 bg-gradient-to-br from-[var(--color-success)] to-[var(--color-primary)] rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg">
|
||||
<CheckCircle className="w-12 h-12 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-[var(--text-primary)] mb-4">
|
||||
¡Felicidades! 🎉
|
||||
</h1>
|
||||
<h2 className="text-xl font-semibold text-[var(--color-success)] mb-4">
|
||||
{data.bakery?.name} está listo para funcionar
|
||||
</h2>
|
||||
<p className="text-[var(--text-secondary)] max-w-3xl mx-auto text-lg leading-relaxed">
|
||||
Has completado exitosamente la configuración inicial de tu panadería inteligente.
|
||||
Tu sistema está optimizado con IA y listo para transformar la gestión de tu negocio.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Completion Stats */}
|
||||
{completionStats && (
|
||||
<Card className="p-6">
|
||||
<div className="text-center mb-6">
|
||||
<div className="inline-flex items-center justify-center w-24 h-24 bg-gradient-to-br from-[var(--color-success)] to-[var(--color-primary)] rounded-full mb-4">
|
||||
<span className="text-2xl font-bold text-white">{completionStats.completionScore}</span>
|
||||
<span className="text-sm text-white/80 ml-1">/100</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
Puntuación de Configuración
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
¡Excelente trabajo! Has superado el promedio de la industria (78/100)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-info)]">{completionStats.totalProducts}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Productos</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-primary)]">{completionStats.inventoryItems}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Inventario</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-success)]">{completionStats.suppliersConfigured}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Proveedores</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-secondary)]">{completionStats.mlModelAccuracy.toFixed(1)}%</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Precisión IA</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-lg font-bold text-[var(--color-warning)]">{completionStats.estimatedTimeSaved}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Tiempo Ahorrado</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Achievement Badges */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4 text-center">
|
||||
🏆 Logros Desbloqueados
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{achievementBadges.map((badge, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`text-center p-4 rounded-lg border-2 transition-all duration-200 ${
|
||||
badge.earned
|
||||
? 'border-[var(--color-success)]/50 bg-[var(--color-success)]/10'
|
||||
: 'border-[var(--border-secondary)] bg-[var(--bg-secondary)] opacity-50'
|
||||
}`}
|
||||
>
|
||||
<div className="text-2xl mb-2">{badge.icon}</div>
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-1 text-sm">{badge.title}</h4>
|
||||
<p className="text-xs text-[var(--text-secondary)]">{badge.description}</p>
|
||||
{badge.earned && (
|
||||
<div className="mt-2">
|
||||
<Badge variant="green" className="text-xs">Conseguido</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
|
||||
🚀 Próximos Pasos
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{quickStartActions.map((action) => (
|
||||
<button
|
||||
key={action.id}
|
||||
onClick={action.action}
|
||||
className={`w-full text-left p-4 rounded-lg border transition-all duration-200 hover:shadow-md ${
|
||||
action.primary
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||
: 'border-[var(--border-secondary)] bg-[var(--bg-secondary)]'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
action.primary ? 'bg-[var(--color-primary)]' : 'bg-[var(--bg-tertiary)]'
|
||||
}`}>
|
||||
<span className={action.primary ? 'text-white' : 'text-[var(--text-secondary)]'}>
|
||||
{action.icon}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className={`font-medium ${
|
||||
action.primary ? 'text-[var(--color-primary)]' : 'text-[var(--text-primary)]'
|
||||
}`}>
|
||||
{action.title}
|
||||
</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{action.description}</p>
|
||||
</div>
|
||||
<ArrowRight className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Additional Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="p-4 text-center">
|
||||
<Download className="w-6 h-6 text-[var(--color-info)] mx-auto mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Certificado</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-3">
|
||||
Descarga tu certificado de configuración
|
||||
</p>
|
||||
<Button size="sm" variant="outline" onClick={generateCertificate}>
|
||||
Descargar
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 text-center">
|
||||
<Calendar className="w-6 h-6 text-[var(--color-primary)] mx-auto mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Demo Personal</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-3">
|
||||
Agenda una demostración 1-a-1
|
||||
</p>
|
||||
<Button size="sm" variant="outline" onClick={scheduleDemo}>
|
||||
Agendar
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 text-center">
|
||||
<Share2 className="w-6 h-6 text-[var(--color-success)] mx-auto mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Compartir Éxito</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-3">
|
||||
Comparte tu logro en redes sociales
|
||||
</p>
|
||||
<Button size="sm" variant="outline" onClick={shareSuccess}>
|
||||
Compartir
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Summary & Thanks */}
|
||||
<Card className="p-6 bg-gradient-to-br from-[var(--color-primary)]/5 to-[var(--color-success)]/5 border-[var(--color-primary)]/20">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-3">
|
||||
🙏 ¡Gracias por confiar en nuestra plataforma!
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] mb-4">
|
||||
Tu panadería ahora cuenta con tecnología de vanguardia para:
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left mb-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||
<span>Predicciones de demanda con IA</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||
<span>Gestión inteligente de inventario</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||
<span>Optimización de compras</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||
<span>Alertas automáticas de restock</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||
<span>Análisis de tendencias de venta</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||
<span>Control multi-tenant seguro</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--color-info)]/10 rounded-lg p-4">
|
||||
<p className="text-sm text-[var(--color-info)]">
|
||||
💡 <strong>Consejo:</strong> Explora el dashboard para descubrir todas las funcionalidades disponibles.
|
||||
El sistema aprenderá de tus patrones y mejorará sus recomendaciones con el tiempo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,467 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Upload, Brain, CheckCircle, AlertCircle, Download, FileText, Activity, TrendingUp } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../ui';
|
||||
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||
|
||||
type ProcessingStage = 'upload' | 'validating' | 'analyzing' | 'completed' | 'error';
|
||||
|
||||
interface ProcessingResult {
|
||||
// Validation data
|
||||
is_valid: boolean;
|
||||
total_records: number;
|
||||
unique_products: number;
|
||||
product_list: string[];
|
||||
validation_errors: string[];
|
||||
validation_warnings: string[];
|
||||
summary: {
|
||||
date_range: string;
|
||||
total_sales: number;
|
||||
average_daily_sales: number;
|
||||
};
|
||||
// Analysis data
|
||||
productsIdentified: number;
|
||||
categoriesDetected: number;
|
||||
businessModel: string;
|
||||
confidenceScore: number;
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
// Unified mock service that handles both validation and analysis
|
||||
const mockDataProcessingService = {
|
||||
processFile: async (file: File, onProgress: (progress: number, stage: string, message: string) => void) => {
|
||||
return new Promise<ProcessingResult>((resolve, reject) => {
|
||||
let progress = 0;
|
||||
|
||||
const stages = [
|
||||
{ threshold: 20, stage: 'validating', message: 'Validando estructura del archivo...' },
|
||||
{ threshold: 40, stage: 'validating', message: 'Verificando integridad de datos...' },
|
||||
{ threshold: 60, stage: 'analyzing', message: 'Identificando productos únicos...' },
|
||||
{ threshold: 80, stage: 'analyzing', message: 'Analizando patrones de venta...' },
|
||||
{ threshold: 90, stage: 'analyzing', message: 'Generando recomendaciones con IA...' },
|
||||
{ threshold: 100, stage: 'completed', message: 'Procesamiento completado' }
|
||||
];
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (progress < 100) {
|
||||
progress += 10;
|
||||
const currentStage = stages.find(s => progress <= s.threshold);
|
||||
if (currentStage) {
|
||||
onProgress(progress, currentStage.stage, currentStage.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (progress >= 100) {
|
||||
clearInterval(interval);
|
||||
// Return combined validation + analysis results
|
||||
resolve({
|
||||
// Validation results
|
||||
is_valid: true,
|
||||
total_records: Math.floor(Math.random() * 1000) + 100,
|
||||
unique_products: Math.floor(Math.random() * 50) + 10,
|
||||
product_list: ['Pan Integral', 'Croissant', 'Baguette', 'Empanadas', 'Pan de Centeno', 'Medialunas'],
|
||||
validation_errors: [],
|
||||
validation_warnings: [
|
||||
'Algunas fechas podrían tener formato inconsistente',
|
||||
'3 productos sin categoría definida'
|
||||
],
|
||||
summary: {
|
||||
date_range: '2024-01-01 to 2024-12-31',
|
||||
total_sales: 15420.50,
|
||||
average_daily_sales: 42.25
|
||||
},
|
||||
// Analysis results
|
||||
productsIdentified: 15,
|
||||
categoriesDetected: 4,
|
||||
businessModel: 'artisan',
|
||||
confidenceScore: 94,
|
||||
recommendations: [
|
||||
'Se detectó un modelo de panadería artesanal con producción propia',
|
||||
'Los productos más vendidos son panes tradicionales y bollería',
|
||||
'Recomendamos categorizar el inventario por tipo de producto',
|
||||
'Considera ampliar la línea de productos de repostería'
|
||||
]
|
||||
});
|
||||
}
|
||||
}, 400);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
onNext,
|
||||
onPrevious,
|
||||
isFirstStep,
|
||||
isLastStep
|
||||
}) => {
|
||||
const [stage, setStage] = useState<ProcessingStage>(data.processingStage || 'upload');
|
||||
const [uploadedFile, setUploadedFile] = useState<File | null>(data.files?.salesData || null);
|
||||
const [progress, setProgress] = useState(data.processingProgress || 0);
|
||||
const [currentMessage, setCurrentMessage] = useState(data.currentMessage || '');
|
||||
const [results, setResults] = useState<ProcessingResult | null>(data.processingResults || null);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Update parent data when state changes
|
||||
onDataChange({
|
||||
...data,
|
||||
processingStage: stage,
|
||||
processingProgress: progress,
|
||||
currentMessage: currentMessage,
|
||||
processingResults: results,
|
||||
files: {
|
||||
...data.files,
|
||||
salesData: uploadedFile
|
||||
}
|
||||
});
|
||||
}, [stage, progress, currentMessage, results, uploadedFile]);
|
||||
|
||||
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);
|
||||
if (files.length > 0) {
|
||||
handleFileUpload(files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
handleFileUpload(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = async (file: File) => {
|
||||
// Validate file type
|
||||
const validExtensions = ['.csv', '.xlsx', '.xls'];
|
||||
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
|
||||
|
||||
if (!validExtensions.includes(fileExtension)) {
|
||||
alert('Formato de archivo no válido. Usa CSV o Excel (.xlsx, .xls)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file size (max 10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
alert('El archivo es demasiado grande. Máximo 10MB permitido.');
|
||||
return;
|
||||
}
|
||||
|
||||
setUploadedFile(file);
|
||||
setStage('validating');
|
||||
setProgress(0);
|
||||
|
||||
try {
|
||||
const result = await mockDataProcessingService.processFile(
|
||||
file,
|
||||
(newProgress, newStage, message) => {
|
||||
setProgress(newProgress);
|
||||
setStage(newStage as ProcessingStage);
|
||||
setCurrentMessage(message);
|
||||
}
|
||||
);
|
||||
|
||||
setResults(result);
|
||||
setStage('completed');
|
||||
} catch (error) {
|
||||
console.error('Processing error:', error);
|
||||
setStage('error');
|
||||
setCurrentMessage('Error en el procesamiento de datos');
|
||||
}
|
||||
};
|
||||
|
||||
const downloadTemplate = () => {
|
||||
const csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
|
||||
2024-01-15,Pan Integral,5,2.50,12.50,Cliente A,Tienda
|
||||
2024-01-15,Croissant,3,1.80,5.40,Cliente B,Online
|
||||
2024-01-15,Baguette,2,3.00,6.00,Cliente C,Tienda
|
||||
2024-01-16,Pan de Centeno,4,2.80,11.20,Cliente A,Tienda
|
||||
2024-01-16,Empanadas,6,4.50,27.00,Cliente D,Delivery`;
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', 'plantilla_ventas.csv');
|
||||
link.style.visibility = 'hidden';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const resetProcess = () => {
|
||||
setStage('upload');
|
||||
setUploadedFile(null);
|
||||
setProgress(0);
|
||||
setCurrentMessage('');
|
||||
setResults(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Improved Upload Stage */}
|
||||
{stage === 'upload' && (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
border-2 border-dashed rounded-2xl p-16 text-center transition-all duration-300 cursor-pointer group
|
||||
${
|
||||
dragActive
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10 scale-[1.02] shadow-lg'
|
||||
: uploadedFile
|
||||
? 'border-[var(--color-success)] bg-[var(--color-success)]/10 shadow-md'
|
||||
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)] hover:bg-[var(--bg-secondary)]/30 hover:shadow-lg'
|
||||
}
|
||||
`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".csv,.xlsx,.xls"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<div className="space-y-8">
|
||||
{uploadedFile ? (
|
||||
<>
|
||||
<div className="w-20 h-20 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center mx-auto">
|
||||
<CheckCircle className="w-10 h-10 text-[var(--color-success)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold text-[var(--color-success)] mb-3">
|
||||
¡Perfecto! Archivo listo
|
||||
</h3>
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 inline-block">
|
||||
<p className="text-[var(--text-primary)] font-medium text-lg">
|
||||
📄 {uploadedFile.name}
|
||||
</p>
|
||||
<p className="text-[var(--text-secondary)] text-sm mt-1">
|
||||
{(uploadedFile.size / 1024 / 1024).toFixed(2)} MB • Listo para procesar
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-20 h-20 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center mx-auto group-hover:scale-110 transition-transform duration-300">
|
||||
<Upload className="w-10 h-10 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold text-[var(--text-primary)] mb-4">
|
||||
Sube tu historial de ventas
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] text-xl leading-relaxed max-w-md mx-auto">
|
||||
Arrastra y suelta tu archivo aquí, o <span className="text-[var(--color-primary)] font-semibold">haz clic para seleccionar</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Visual indicators */}
|
||||
<div className="flex justify-center space-x-8 mt-8">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mx-auto mb-2">
|
||||
<span className="text-2xl">📊</span>
|
||||
</div>
|
||||
<span className="text-sm text-[var(--text-secondary)]">CSV</span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mx-auto mb-2">
|
||||
<span className="text-2xl">📈</span>
|
||||
</div>
|
||||
<span className="text-sm text-[var(--text-secondary)]">Excel</span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mx-auto mb-2">
|
||||
<span className="text-2xl">⚡</span>
|
||||
</div>
|
||||
<span className="text-sm text-[var(--text-secondary)]">Hasta 10MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-10 px-4 py-2 bg-[var(--bg-secondary)]/50 rounded-lg text-sm text-[var(--text-tertiary)] inline-block">
|
||||
💡 Formatos aceptados: CSV, Excel (XLSX, XLS) • Tamaño máximo: 10MB
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Improved Template Download Section */}
|
||||
<div className="bg-gradient-to-r from-[var(--color-info)]/5 to-[var(--color-primary)]/5 rounded-xl p-6 border border-[var(--color-info)]/20">
|
||||
<div className="flex flex-col md:flex-row items-center space-y-4 md:space-y-0 md:space-x-6">
|
||||
<div className="w-16 h-16 rounded-full bg-[var(--color-info)]/10 flex items-center justify-center flex-shrink-0">
|
||||
<Download className="w-8 h-8 text-[var(--color-info)]" />
|
||||
</div>
|
||||
<div className="flex-1 text-center md:text-left">
|
||||
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
¿Necesitas ayuda con el formato?
|
||||
</h4>
|
||||
<p className="text-[var(--text-secondary)] mb-4">
|
||||
Descarga nuestra plantilla Excel con ejemplos y formato correcto para tus datos de ventas
|
||||
</p>
|
||||
<Button
|
||||
onClick={downloadTemplate}
|
||||
className="bg-[var(--color-info)] hover:bg-[var(--color-info)]/90 text-white shadow-lg"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Descargar Plantilla Gratuita
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Processing Stages */}
|
||||
{(stage === 'validating' || stage === 'analyzing') && (
|
||||
<Card className="p-8">
|
||||
<div className="text-center">
|
||||
<div className="relative mb-8">
|
||||
<div className={`w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6 ${
|
||||
stage === 'validating'
|
||||
? 'bg-[var(--color-info)]/10 animate-pulse'
|
||||
: 'bg-[var(--color-primary)]/10 animate-pulse'
|
||||
}`}>
|
||||
{stage === 'validating' ? (
|
||||
<FileText className={`w-8 h-8 ${stage === 'validating' ? 'text-[var(--color-info)]' : 'text-[var(--color-primary)]'}`} />
|
||||
) : (
|
||||
<Brain className="w-8 h-8 text-[var(--color-primary)]" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-semibold text-[var(--text-primary)] mb-2">
|
||||
{stage === 'validating' ? 'Validando datos...' : 'Analizando con IA...'}
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] mb-8">
|
||||
{currentMessage}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
Progreso
|
||||
</span>
|
||||
<span className="text-sm text-[var(--text-secondary)]">{progress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-3">
|
||||
<div
|
||||
className="bg-gradient-to-r from-[var(--color-info)] to-[var(--color-primary)] h-3 rounded-full transition-all duration-500 ease-out"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Processing Steps */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className={`p-4 rounded-lg text-center ${
|
||||
progress >= 40 ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' : 'bg-[var(--bg-secondary)]'
|
||||
}`}>
|
||||
<FileText className="w-6 h-6 mx-auto mb-2" />
|
||||
<span className="text-sm font-medium">Validación</span>
|
||||
</div>
|
||||
<div className={`p-4 rounded-lg text-center ${
|
||||
progress >= 70 ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' : 'bg-[var(--bg-secondary)]'
|
||||
}`}>
|
||||
<Brain className="w-6 h-6 mx-auto mb-2" />
|
||||
<span className="text-sm font-medium">Análisis IA</span>
|
||||
</div>
|
||||
<div className={`p-4 rounded-lg text-center ${
|
||||
progress >= 100 ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' : 'bg-[var(--bg-secondary)]'
|
||||
}`}>
|
||||
<CheckCircle className="w-6 h-6 mx-auto mb-2" />
|
||||
<span className="text-sm font-medium">Completo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Simplified Results Stage */}
|
||||
{stage === 'completed' && results && (
|
||||
<div className="space-y-8">
|
||||
{/* Success Header */}
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-[var(--color-success)] rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<CheckCircle className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold text-[var(--color-success)] mb-3">
|
||||
¡Procesamiento Completado!
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] max-w-2xl mx-auto">
|
||||
Tus datos han sido procesados exitosamente
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Simple Stats Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="text-center p-4 bg-[var(--color-info)]/10 rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-info)]">{results.total_records}</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Registros</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-[var(--color-primary)]/10 rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-primary)]">{results.productsIdentified}</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Productos</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-success)]">{results.confidenceScore}%</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Confianza</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-lg font-bold text-[var(--text-primary)]">
|
||||
{results.businessModel === 'artisan' ? 'Artesanal' :
|
||||
results.businessModel === 'retail' ? 'Retail' : 'Híbrido'}
|
||||
</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Modelo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{stage === 'error' && (
|
||||
<Card className="p-8 text-center">
|
||||
<div className="w-16 h-16 bg-[var(--color-error)] rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<AlertCircle className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-[var(--color-error)] mb-3">
|
||||
Error en el procesamiento
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] mb-6">
|
||||
{currentMessage}
|
||||
</p>
|
||||
<Button onClick={resetProcess} variant="outline">
|
||||
Intentar nuevamente
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,565 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Package, Calendar, AlertTriangle, Plus, Edit, Trash2 } from 'lucide-react';
|
||||
import { Button, Card, Input, Badge } from '../../../ui';
|
||||
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||
|
||||
interface InventoryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
category: 'ingredient' | 'finished_product';
|
||||
current_stock: number;
|
||||
min_stock: number;
|
||||
max_stock: number;
|
||||
unit: string;
|
||||
expiry_date?: string;
|
||||
supplier?: string;
|
||||
cost_per_unit?: number;
|
||||
requires_refrigeration: boolean;
|
||||
}
|
||||
|
||||
// Mock inventory items based on approved products
|
||||
const mockInventoryItems: InventoryItem[] = [
|
||||
{
|
||||
id: '1', name: 'Harina de Trigo', category: 'ingredient',
|
||||
current_stock: 50, min_stock: 20, max_stock: 100, unit: 'kg',
|
||||
expiry_date: '2024-12-31', supplier: 'Molinos del Sur',
|
||||
cost_per_unit: 1.20, requires_refrigeration: false
|
||||
},
|
||||
{
|
||||
id: '2', name: 'Levadura Fresca', category: 'ingredient',
|
||||
current_stock: 5, min_stock: 2, max_stock: 10, unit: 'kg',
|
||||
expiry_date: '2024-03-15', supplier: 'Levaduras Pro',
|
||||
cost_per_unit: 3.50, requires_refrigeration: true
|
||||
},
|
||||
{
|
||||
id: '3', name: 'Pan Integral', category: 'finished_product',
|
||||
current_stock: 20, min_stock: 10, max_stock: 50, unit: 'unidades',
|
||||
expiry_date: '2024-01-25', requires_refrigeration: false
|
||||
},
|
||||
{
|
||||
id: '4', name: 'Mantequilla', category: 'ingredient',
|
||||
current_stock: 15, min_stock: 5, max_stock: 30, unit: 'kg',
|
||||
expiry_date: '2024-02-28', supplier: 'Lácteos Premium',
|
||||
cost_per_unit: 4.20, requires_refrigeration: true
|
||||
}
|
||||
];
|
||||
|
||||
export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
onNext,
|
||||
onPrevious,
|
||||
isFirstStep,
|
||||
isLastStep
|
||||
}) => {
|
||||
const [items, setItems] = useState<InventoryItem[]>(
|
||||
data.inventoryItems || mockInventoryItems
|
||||
);
|
||||
const [editingItem, setEditingItem] = useState<InventoryItem | null>(null);
|
||||
const [isAddingNew, setIsAddingNew] = useState(false);
|
||||
const [filterCategory, setFilterCategory] = useState<'all' | 'ingredient' | 'finished_product'>('all');
|
||||
|
||||
useEffect(() => {
|
||||
onDataChange({
|
||||
...data,
|
||||
inventoryItems: items,
|
||||
inventoryConfigured: items.length > 0 && items.every(item =>
|
||||
item.min_stock > 0 && item.max_stock > item.min_stock
|
||||
)
|
||||
});
|
||||
}, [items]);
|
||||
|
||||
const handleAddItem = () => {
|
||||
const newItem: InventoryItem = {
|
||||
id: Date.now().toString(),
|
||||
name: '',
|
||||
category: 'ingredient',
|
||||
current_stock: 0,
|
||||
min_stock: 0,
|
||||
max_stock: 0,
|
||||
unit: 'kg',
|
||||
requires_refrigeration: false
|
||||
};
|
||||
setEditingItem(newItem);
|
||||
setIsAddingNew(true);
|
||||
};
|
||||
|
||||
const handleSaveItem = (item: InventoryItem) => {
|
||||
if (isAddingNew) {
|
||||
setItems(prev => [...prev, item]);
|
||||
} else {
|
||||
setItems(prev => prev.map(i => i.id === item.id ? item : i));
|
||||
}
|
||||
setEditingItem(null);
|
||||
setIsAddingNew(false);
|
||||
};
|
||||
|
||||
const handleDeleteItem = (id: string) => {
|
||||
if (window.confirm('¿Estás seguro de eliminar este elemento del inventario?')) {
|
||||
setItems(prev => prev.filter(item => item.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickSetup = () => {
|
||||
// Auto-configure basic inventory based on approved products
|
||||
const autoItems = data.detectedProducts
|
||||
?.filter((p: any) => p.status === 'approved')
|
||||
.map((product: any, index: number) => ({
|
||||
id: `auto_${index}`,
|
||||
name: product.name,
|
||||
category: 'finished_product' as const,
|
||||
current_stock: Math.floor(Math.random() * 20) + 5,
|
||||
min_stock: 5,
|
||||
max_stock: 50,
|
||||
unit: 'unidades',
|
||||
requires_refrigeration: product.category === 'Repostería' || product.category === 'Salados'
|
||||
})) || [];
|
||||
|
||||
setItems(prev => [...prev, ...autoItems]);
|
||||
};
|
||||
|
||||
const getFilteredItems = () => {
|
||||
return filterCategory === 'all'
|
||||
? items
|
||||
: items.filter(item => item.category === filterCategory);
|
||||
};
|
||||
|
||||
const getStockStatus = (item: InventoryItem) => {
|
||||
if (item.current_stock <= item.min_stock) return { status: 'low', color: 'red', text: 'Stock Bajo' };
|
||||
if (item.current_stock >= item.max_stock) return { status: 'high', color: 'blue', text: 'Stock Alto' };
|
||||
return { status: 'normal', color: 'green', text: 'Normal' };
|
||||
};
|
||||
|
||||
const isNearExpiry = (expiryDate?: string) => {
|
||||
if (!expiryDate) return false;
|
||||
const expiry = new Date(expiryDate);
|
||||
const today = new Date();
|
||||
const diffDays = (expiry.getTime() - today.getTime()) / (1000 * 3600 * 24);
|
||||
return diffDays <= 7;
|
||||
};
|
||||
|
||||
const stats = {
|
||||
total: items.length,
|
||||
ingredients: items.filter(i => i.category === 'ingredient').length,
|
||||
products: items.filter(i => i.category === 'finished_product').length,
|
||||
lowStock: items.filter(i => i.current_stock <= i.min_stock).length,
|
||||
nearExpiry: items.filter(i => isNearExpiry(i.expiry_date)).length,
|
||||
refrigerated: items.filter(i => i.requires_refrigeration).length
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Quick Actions */}
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{stats.total} elementos configurados
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleQuickSetup}
|
||||
className="text-[var(--color-info)]"
|
||||
>
|
||||
Auto-configurar Productos
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleAddItem}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Agregar Elemento
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-6 gap-4">
|
||||
<Card className="p-4 text-center">
|
||||
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.total}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Total</p>
|
||||
</Card>
|
||||
<Card className="p-4 text-center">
|
||||
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.ingredients}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Ingredientes</p>
|
||||
</Card>
|
||||
<Card className="p-4 text-center">
|
||||
<p className="text-2xl font-bold text-[var(--color-success)]">{stats.products}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Productos</p>
|
||||
</Card>
|
||||
<Card className="p-4 text-center">
|
||||
<p className="text-2xl font-bold text-[var(--color-error)]">{stats.lowStock}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Stock Bajo</p>
|
||||
</Card>
|
||||
<Card className="p-4 text-center">
|
||||
<p className="text-2xl font-bold text-[var(--color-warning)]">{stats.nearExpiry}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Por Vencer</p>
|
||||
</Card>
|
||||
<Card className="p-4 text-center">
|
||||
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.refrigerated}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Refrigerado</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">Filtrar:</label>
|
||||
<div className="flex space-x-2">
|
||||
{[
|
||||
{ value: 'all', label: 'Todos' },
|
||||
{ value: 'ingredient', label: 'Ingredientes' },
|
||||
{ value: 'finished_product', label: 'Productos' }
|
||||
].map(filter => (
|
||||
<button
|
||||
key={filter.value}
|
||||
onClick={() => setFilterCategory(filter.value as any)}
|
||||
className={`px-3 py-1 text-sm rounded-full transition-colors ${
|
||||
filterCategory === filter.value
|
||||
? 'bg-[var(--color-primary)] text-white'
|
||||
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
|
||||
}`}
|
||||
>
|
||||
{filter.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Inventory Items */}
|
||||
<div className="space-y-4">
|
||||
{getFilteredItems().map((item) => {
|
||||
const stockStatus = getStockStatus(item);
|
||||
const nearExpiry = isNearExpiry(item.expiry_date);
|
||||
|
||||
return (
|
||||
<Card key={item.id} className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-4 flex-1">
|
||||
{/* Category Icon */}
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center mt-1 ${
|
||||
item.category === 'ingredient'
|
||||
? 'bg-[var(--color-primary)]/10'
|
||||
: 'bg-[var(--color-success)]/10'
|
||||
}`}>
|
||||
<Package className={`w-4 h-4 ${
|
||||
item.category === 'ingredient'
|
||||
? 'text-[var(--color-primary)]'
|
||||
: 'text-[var(--color-success)]'
|
||||
}`} />
|
||||
</div>
|
||||
|
||||
{/* Item Info */}
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{item.name}</h4>
|
||||
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Badge variant={item.category === 'ingredient' ? 'blue' : 'green'}>
|
||||
{item.category === 'ingredient' ? 'Ingrediente' : 'Producto'}
|
||||
</Badge>
|
||||
{item.requires_refrigeration && (
|
||||
<Badge variant="gray">❄️ Refrigeración</Badge>
|
||||
)}
|
||||
<Badge variant={stockStatus.color}>
|
||||
{stockStatus.text}
|
||||
</Badge>
|
||||
{nearExpiry && (
|
||||
<Badge variant="red">Vence Pronto</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 text-sm text-[var(--text-secondary)]">
|
||||
<div>
|
||||
<span className="text-[var(--text-tertiary)]">Stock Actual: </span>
|
||||
<span className={`font-medium ${stockStatus.status === 'low' ? 'text-[var(--color-error)]' : 'text-[var(--text-primary)]'}`}>
|
||||
{item.current_stock} {item.unit}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-[var(--text-tertiary)]">Rango: </span>
|
||||
<span className="font-medium text-[var(--text-primary)]">
|
||||
{item.min_stock} - {item.max_stock} {item.unit}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{item.expiry_date && (
|
||||
<div>
|
||||
<span className="text-[var(--text-tertiary)]">Vencimiento: </span>
|
||||
<span className={`font-medium ${nearExpiry ? 'text-[var(--color-error)]' : 'text-[var(--text-primary)]'}`}>
|
||||
{new Date(item.expiry_date).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.cost_per_unit && (
|
||||
<div>
|
||||
<span className="text-[var(--text-tertiary)]">Costo/Unidad: </span>
|
||||
<span className="font-medium text-[var(--text-primary)]">
|
||||
${item.cost_per_unit.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 ml-4 mt-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setEditingItem(item)}
|
||||
>
|
||||
<Edit className="w-4 h-4 mr-1" />
|
||||
Editar
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleDeleteItem(item.id)}
|
||||
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
{getFilteredItems().length === 0 && (
|
||||
<Card className="p-8 text-center">
|
||||
<Package className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
|
||||
<p className="text-[var(--text-secondary)]">No hay elementos en esta categoría</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Warnings */}
|
||||
{(stats.lowStock > 0 || stats.nearExpiry > 0) && (
|
||||
<Card className="p-4 bg-[var(--color-warning-50)] border-[var(--color-warning-200)]">
|
||||
<div className="flex items-start space-x-3">
|
||||
<AlertTriangle className="w-5 h-5 text-[var(--color-warning)] flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-[var(--color-warning-800)] mb-1">Advertencias de Inventario</h4>
|
||||
{stats.lowStock > 0 && (
|
||||
<p className="text-sm text-[var(--color-warning-700)] mb-1">
|
||||
• {stats.lowStock} elemento(s) con stock bajo
|
||||
</p>
|
||||
)}
|
||||
{stats.nearExpiry > 0 && (
|
||||
<p className="text-sm text-[var(--color-warning-700)]">
|
||||
• {stats.nearExpiry} elemento(s) próximos a vencer
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Edit Modal */}
|
||||
{editingItem && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<Card className="p-6 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
|
||||
{isAddingNew ? 'Agregar Elemento' : 'Editar Elemento'}
|
||||
</h3>
|
||||
|
||||
<InventoryItemForm
|
||||
item={editingItem}
|
||||
onSave={handleSaveItem}
|
||||
onCancel={() => {
|
||||
setEditingItem(null);
|
||||
setIsAddingNew(false);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Information */}
|
||||
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
||||
<h4 className="font-medium text-[var(--color-info)] mb-2">
|
||||
📦 Configuración de Inventario:
|
||||
</h4>
|
||||
<ul className="text-sm text-[var(--color-info)] space-y-1">
|
||||
<li>• <strong>Stock Mínimo:</strong> Nivel que dispara alertas de reabastecimiento</li>
|
||||
<li>• <strong>Stock Máximo:</strong> Capacidad máxima de almacenamiento</li>
|
||||
<li>• <strong>Fechas de Vencimiento:</strong> Control automático de productos perecederos</li>
|
||||
<li>• <strong>Refrigeración:</strong> Identifica productos que requieren frío</li>
|
||||
</ul>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Component for editing inventory items
|
||||
interface InventoryItemFormProps {
|
||||
item: InventoryItem;
|
||||
onSave: (item: InventoryItem) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const InventoryItemForm: React.FC<InventoryItemFormProps> = ({ item, onSave, onCancel }) => {
|
||||
const [formData, setFormData] = useState(item);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.name.trim()) {
|
||||
alert('El nombre es requerido');
|
||||
return;
|
||||
}
|
||||
if (formData.min_stock >= formData.max_stock) {
|
||||
alert('El stock máximo debe ser mayor al mínimo');
|
||||
return;
|
||||
}
|
||||
onSave(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Nombre *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="Nombre del producto/ingrediente"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Categoría *
|
||||
</label>
|
||||
<select
|
||||
value={formData.category}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value as any }))}
|
||||
className="w-full p-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||
>
|
||||
<option value="ingredient">Ingrediente</option>
|
||||
<option value="finished_product">Producto Terminado</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Unidad *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.unit}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, unit: e.target.value }))}
|
||||
placeholder="kg, unidades, litros..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Stock Actual
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.current_stock}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, current_stock: Number(e.target.value) }))}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Stock Mín. *
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.min_stock}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, min_stock: Number(e.target.value) }))}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Stock Máx. *
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.max_stock}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, max_stock: Number(e.target.value) }))}
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Fecha Vencimiento
|
||||
</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.expiry_date || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, expiry_date: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Costo por Unidad
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.cost_per_unit || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, cost_per_unit: Number(e.target.value) }))}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Proveedor
|
||||
</label>
|
||||
<Input
|
||||
value={formData.supplier || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, supplier: e.target.value }))}
|
||||
placeholder="Nombre del proveedor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="refrigeration"
|
||||
checked={formData.requires_refrigeration}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, requires_refrigeration: e.target.checked }))}
|
||||
className="rounded"
|
||||
/>
|
||||
<label htmlFor="refrigeration" className="text-sm text-[var(--text-primary)]">
|
||||
Requiere refrigeración
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
Guardar
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,780 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Brain, Activity, Zap, CheckCircle, AlertCircle, TrendingUp } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../ui';
|
||||
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||
|
||||
interface TrainingMetrics {
|
||||
accuracy: number;
|
||||
precision: number;
|
||||
recall: number;
|
||||
f1_score: number;
|
||||
training_loss: number;
|
||||
validation_loss: number;
|
||||
epochs_completed: number;
|
||||
total_epochs: number;
|
||||
}
|
||||
|
||||
interface TrainingLog {
|
||||
timestamp: string;
|
||||
message: string;
|
||||
level: 'info' | 'warning' | 'error' | 'success';
|
||||
}
|
||||
|
||||
// Enhanced Mock ML training service that matches backend behavior
|
||||
const mockMLService = {
|
||||
startTraining: async (trainingData: any) => {
|
||||
return new Promise((resolve) => {
|
||||
let progress = 0;
|
||||
let step_count = 0;
|
||||
const total_steps = 12;
|
||||
|
||||
// Backend-matching training steps and messages
|
||||
const trainingSteps = [
|
||||
{ step: 'data_validation', message: 'Validando datos de entrenamiento...', progress: 10 },
|
||||
{ step: 'data_preparation_start', message: 'Preparando conjunto de datos...', progress: 20 },
|
||||
{ step: 'feature_engineering', message: 'Creando características para el modelo...', progress: 30 },
|
||||
{ step: 'data_preparation_complete', message: 'Preparación de datos completada', progress: 35 },
|
||||
{ step: 'ml_training_start', message: 'Iniciando entrenamiento del modelo Prophet...', progress: 40 },
|
||||
{ step: 'model_fitting', message: 'Ajustando modelo a los datos históricos...', progress: 55 },
|
||||
{ step: 'pattern_detection', message: 'Detectando patrones estacionales y tendencias...', progress: 70 },
|
||||
{ step: 'validation', message: 'Validando precisión del modelo...', progress: 80 },
|
||||
{ step: 'training_complete', message: 'Entrenamiento ML completado', progress: 85 },
|
||||
{ step: 'storing_models', message: 'Guardando modelo entrenado...', progress: 90 },
|
||||
{ step: 'performance_metrics', message: 'Calculando métricas de rendimiento...', progress: 95 },
|
||||
{ step: 'completed', message: 'Tu Asistente Inteligente está listo!', progress: 100 }
|
||||
];
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (step_count >= trainingSteps.length) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentStep = trainingSteps[step_count];
|
||||
progress = currentStep.progress;
|
||||
|
||||
// Generate realistic metrics that improve over time
|
||||
const stepProgress = step_count / trainingSteps.length;
|
||||
const baseAccuracy = 0.65;
|
||||
const maxAccuracy = 0.93;
|
||||
const currentAccuracy = baseAccuracy + (maxAccuracy - baseAccuracy) * Math.pow(stepProgress, 0.8);
|
||||
|
||||
const metrics: TrainingMetrics = {
|
||||
accuracy: Math.min(maxAccuracy, currentAccuracy + (Math.random() * 0.02 - 0.01)),
|
||||
precision: Math.min(0.95, 0.70 + stepProgress * 0.23 + (Math.random() * 0.02 - 0.01)),
|
||||
recall: Math.min(0.94, 0.68 + stepProgress * 0.24 + (Math.random() * 0.02 - 0.01)),
|
||||
f1_score: Math.min(0.94, 0.69 + stepProgress * 0.23 + (Math.random() * 0.02 - 0.01)),
|
||||
training_loss: Math.max(0.08, 1.2 - stepProgress * 1.0 + (Math.random() * 0.05 - 0.025)),
|
||||
validation_loss: Math.max(0.10, 1.3 - stepProgress * 1.1 + (Math.random() * 0.06 - 0.03)),
|
||||
epochs_completed: Math.floor(stepProgress * 15) + 1,
|
||||
total_epochs: 15
|
||||
};
|
||||
|
||||
// Generate step-specific logs that match backend behavior
|
||||
const logs: TrainingLog[] = [];
|
||||
|
||||
// Add the current step message
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: currentStep.message,
|
||||
level: currentStep.step === 'completed' ? 'success' : 'info'
|
||||
});
|
||||
|
||||
// Add specific logs based on the training step
|
||||
if (currentStep.step === 'data_validation') {
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: `Productos analizados: ${trainingData.products?.length || 3}`,
|
||||
level: 'info'
|
||||
});
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: `Registros de inventario: ${trainingData.inventory?.length || 3}`,
|
||||
level: 'info'
|
||||
});
|
||||
} else if (currentStep.step === 'ml_training_start') {
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: 'Usando modelo Prophet optimizado para panadería',
|
||||
level: 'info'
|
||||
});
|
||||
} else if (currentStep.step === 'model_fitting') {
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: `Precisión actual: ${(metrics.accuracy * 100).toFixed(1)}%`,
|
||||
level: 'info'
|
||||
});
|
||||
} else if (currentStep.step === 'pattern_detection') {
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: 'Patrones estacionales detectados: días de la semana, horas pico',
|
||||
level: 'success'
|
||||
});
|
||||
} else if (currentStep.step === 'validation') {
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: `MAE: ${(Math.random() * 2 + 1.5).toFixed(2)}, MAPE: ${((1 - metrics.accuracy) * 100).toFixed(1)}%`,
|
||||
level: 'info'
|
||||
});
|
||||
} else if (currentStep.step === 'storing_models') {
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: `Modelo guardado: bakery_prophet_${Date.now()}`,
|
||||
level: 'success'
|
||||
});
|
||||
}
|
||||
|
||||
// Emit progress event that matches backend WebSocket message structure
|
||||
window.dispatchEvent(new CustomEvent('mlTrainingProgress', {
|
||||
detail: {
|
||||
type: 'progress',
|
||||
job_id: `training_${Date.now()}`,
|
||||
data: {
|
||||
progress,
|
||||
current_step: currentStep.step,
|
||||
step_details: currentStep.message,
|
||||
products_completed: Math.floor(step_count / 3),
|
||||
products_total: Math.max(3, trainingData.products?.length || 3),
|
||||
estimated_time_remaining_minutes: Math.max(0, Math.floor((total_steps - step_count) * 0.75))
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
// Keep old format for backward compatibility
|
||||
status: progress >= 100 ? 'completed' : 'training',
|
||||
currentPhase: currentStep.message,
|
||||
metrics,
|
||||
logs
|
||||
}
|
||||
}));
|
||||
|
||||
step_count++;
|
||||
|
||||
if (progress >= 100) {
|
||||
clearInterval(interval);
|
||||
|
||||
// Generate final results matching backend response structure
|
||||
const finalAccuracy = 0.89 + Math.random() * 0.05;
|
||||
const finalResult = {
|
||||
success: true,
|
||||
job_id: `training_${Date.now()}`,
|
||||
tenant_id: 'demo_tenant',
|
||||
status: 'completed',
|
||||
training_results: {
|
||||
total_products: Math.max(3, trainingData.products?.length || 3),
|
||||
successful_trainings: Math.max(3, trainingData.products?.length || 3),
|
||||
failed_trainings: 0,
|
||||
products: Array.from({length: Math.max(3, trainingData.products?.length || 3)}, (_, i) => ({
|
||||
inventory_product_id: `product_${i + 1}`,
|
||||
status: 'completed',
|
||||
model_id: `model_${i + 1}_${Date.now()}`,
|
||||
data_points: 45 + Math.floor(Math.random() * 30),
|
||||
metrics: {
|
||||
mape: (1 - finalAccuracy) * 100 + (Math.random() * 2 - 1),
|
||||
mae: 1.5 + Math.random() * 1.0,
|
||||
rmse: 2.1 + Math.random() * 1.2,
|
||||
r2_score: finalAccuracy + (Math.random() * 0.02 - 0.01)
|
||||
}
|
||||
})),
|
||||
overall_training_time_seconds: 45 + Math.random() * 15
|
||||
},
|
||||
data_summary: {
|
||||
sales_records: trainingData.inventory?.length * 30 || 1500,
|
||||
weather_records: 90,
|
||||
traffic_records: 85,
|
||||
date_range: {
|
||||
start: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
end: new Date().toISOString()
|
||||
},
|
||||
data_sources_used: ['bakery_sales', 'weather_forecast', 'madrid_traffic'],
|
||||
constraints_applied: {}
|
||||
},
|
||||
finalMetrics: {
|
||||
accuracy: finalAccuracy,
|
||||
precision: finalAccuracy * 0.98,
|
||||
recall: finalAccuracy * 0.96,
|
||||
f1_score: finalAccuracy * 0.97,
|
||||
training_loss: 0.12 + Math.random() * 0.05,
|
||||
validation_loss: 0.15 + Math.random() * 0.05,
|
||||
total_epochs: 15
|
||||
},
|
||||
modelId: `bakery_prophet_${Date.now()}`,
|
||||
deploymentUrl: '/api/v1/training/models/predict',
|
||||
trainingDuration: `${(45 + Math.random() * 15).toFixed(1)} segundos`,
|
||||
datasetSize: `${trainingData.inventory?.length * 30 || 1500} registros`,
|
||||
modelVersion: '2.1.0',
|
||||
completed_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
resolve(finalResult);
|
||||
}
|
||||
}, 1800); // Matches backend timing
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getTrainingPhase = (epoch: number, total: number) => {
|
||||
const progress = epoch / total;
|
||||
if (progress <= 0.3) return 'Inicializando modelo de IA...';
|
||||
if (progress <= 0.6) return 'Entrenando patrones de venta...';
|
||||
if (progress <= 0.8) return 'Optimizando predicciones...';
|
||||
if (progress <= 0.95) return 'Validando rendimiento...';
|
||||
return 'Finalizando entrenamiento...';
|
||||
};
|
||||
|
||||
export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
onNext,
|
||||
onPrevious,
|
||||
isFirstStep,
|
||||
isLastStep
|
||||
}) => {
|
||||
const [trainingStatus, setTrainingStatus] = useState(data.trainingStatus || 'pending');
|
||||
const [progress, setProgress] = useState(data.trainingProgress || 0);
|
||||
const [currentPhase, setCurrentPhase] = useState(data.currentPhase || '');
|
||||
const [metrics, setMetrics] = useState<TrainingMetrics | null>(data.trainingMetrics || null);
|
||||
const [logs, setLogs] = useState<TrainingLog[]>(data.trainingLogs || []);
|
||||
const [finalResults, setFinalResults] = useState<any>(data.finalResults || null);
|
||||
|
||||
// New state variables for backend-compatible data
|
||||
const [productsCompleted, setProductsCompleted] = useState(0);
|
||||
const [productsTotal, setProductsTotal] = useState(0);
|
||||
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for ML training progress updates
|
||||
const handleProgress = (event: CustomEvent) => {
|
||||
const detail = event.detail;
|
||||
|
||||
// Handle new backend-compatible WebSocket message format
|
||||
if (detail.type === 'progress' && detail.data) {
|
||||
const progressData = detail.data;
|
||||
setProgress(progressData.progress || 0);
|
||||
setCurrentPhase(progressData.step_details || progressData.current_step || 'En progreso...');
|
||||
|
||||
// Update products progress if available
|
||||
if (progressData.products_completed !== undefined) {
|
||||
setProductsCompleted(progressData.products_completed);
|
||||
}
|
||||
if (progressData.products_total !== undefined) {
|
||||
setProductsTotal(progressData.products_total);
|
||||
}
|
||||
|
||||
// Update time estimate if available
|
||||
if (progressData.estimated_time_remaining_minutes !== undefined) {
|
||||
setEstimatedTimeRemaining(progressData.estimated_time_remaining_minutes);
|
||||
}
|
||||
|
||||
// Handle metrics (from legacy format for backward compatibility)
|
||||
if (detail.metrics) {
|
||||
setMetrics(detail.metrics);
|
||||
}
|
||||
|
||||
// Handle logs (from legacy format for backward compatibility)
|
||||
if (detail.logs && detail.logs.length > 0) {
|
||||
setLogs(prev => [...prev, ...detail.logs]);
|
||||
}
|
||||
|
||||
// Check completion status
|
||||
if (progressData.progress >= 100) {
|
||||
setTrainingStatus('completed');
|
||||
} else {
|
||||
setTrainingStatus('training');
|
||||
}
|
||||
} else {
|
||||
// Handle legacy format for backward compatibility
|
||||
const { progress: newProgress, metrics: newMetrics, logs: newLogs, status, currentPhase: newPhase } = detail;
|
||||
|
||||
setProgress(newProgress || 0);
|
||||
setMetrics(newMetrics);
|
||||
setCurrentPhase(newPhase || 'En progreso...');
|
||||
setTrainingStatus(status || 'training');
|
||||
|
||||
if (newLogs && newLogs.length > 0) {
|
||||
setLogs(prev => [...prev, ...newLogs]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mlTrainingProgress', handleProgress as EventListener);
|
||||
return () => window.removeEventListener('mlTrainingProgress', handleProgress as EventListener);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Update parent data when state changes
|
||||
onDataChange({
|
||||
...data,
|
||||
trainingStatus,
|
||||
trainingProgress: progress,
|
||||
currentPhase,
|
||||
trainingMetrics: metrics,
|
||||
trainingLogs: logs,
|
||||
finalResults
|
||||
});
|
||||
}, [trainingStatus, progress, currentPhase, metrics, logs, finalResults]);
|
||||
|
||||
// Auto-start training when coming from suppliers step
|
||||
useEffect(() => {
|
||||
if (data.autoStartTraining && trainingStatus === 'pending') {
|
||||
const timer = setTimeout(() => {
|
||||
startTraining();
|
||||
// Remove the auto-start flag so it doesn't trigger again
|
||||
onDataChange({ ...data, autoStartTraining: false });
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [data.autoStartTraining, trainingStatus]);
|
||||
|
||||
const startTraining = async () => {
|
||||
// Access data from previous steps through allStepData
|
||||
const inventoryData = data.allStepData?.inventory?.inventoryItems ||
|
||||
data.allStepData?.['inventory-setup']?.inventoryItems ||
|
||||
data.inventoryItems || [];
|
||||
|
||||
const detectedProducts = data.allStepData?.review?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
|
||||
data.allStepData?.['data-processing']?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
|
||||
data.detectedProducts?.filter((p: any) => p.status === 'approved') || [];
|
||||
|
||||
const salesValidation = data.allStepData?.['data-processing']?.validation ||
|
||||
data.allStepData?.review?.validation ||
|
||||
data.validation || {};
|
||||
|
||||
// If no data is available, create mock data for demo purposes
|
||||
let finalInventoryData = inventoryData;
|
||||
let finalDetectedProducts = detectedProducts;
|
||||
let finalValidation = salesValidation;
|
||||
|
||||
if (inventoryData.length === 0 && detectedProducts.length === 0) {
|
||||
console.log('No data found from previous steps, using mock data for demo');
|
||||
|
||||
// Create mock data for demonstration
|
||||
finalInventoryData = [
|
||||
{ id: '1', name: 'Harina de Trigo', category: 'ingredient', current_stock: 50, min_stock: 20, max_stock: 100, unit: 'kg' },
|
||||
{ id: '2', name: 'Levadura Fresca', category: 'ingredient', current_stock: 5, min_stock: 2, max_stock: 10, unit: 'kg' },
|
||||
{ id: '3', name: 'Pan Integral', category: 'finished_product', current_stock: 20, min_stock: 10, max_stock: 50, unit: 'unidades' }
|
||||
];
|
||||
|
||||
finalDetectedProducts = [
|
||||
{ name: 'Pan Francés', status: 'approved', category: 'Panadería' },
|
||||
{ name: 'Croissants', status: 'approved', category: 'Repostería' },
|
||||
{ name: 'Pan Integral', status: 'approved', category: 'Panadería' }
|
||||
];
|
||||
|
||||
finalValidation = {
|
||||
total_records: 1500,
|
||||
summary: {
|
||||
date_range: '2024-01-01 to 2024-12-31'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setTrainingStatus('training');
|
||||
setProgress(0);
|
||||
setLogs([{
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: 'Iniciando entrenamiento del modelo de Machine Learning...',
|
||||
level: 'info'
|
||||
}]);
|
||||
|
||||
try {
|
||||
const result = await mockMLService.startTraining({
|
||||
products: finalDetectedProducts,
|
||||
inventory: finalInventoryData,
|
||||
salesData: finalValidation
|
||||
});
|
||||
|
||||
setFinalResults(result);
|
||||
setTrainingStatus('completed');
|
||||
|
||||
} catch (error) {
|
||||
console.error('ML Training error:', error);
|
||||
setTrainingStatus('error');
|
||||
setLogs(prev => [...prev, {
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: 'Error durante el entrenamiento del modelo',
|
||||
level: 'error'
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
const retryTraining = () => {
|
||||
setTrainingStatus('pending');
|
||||
setProgress(0);
|
||||
setMetrics(null);
|
||||
setLogs([]);
|
||||
setFinalResults(null);
|
||||
};
|
||||
|
||||
const getLogIcon = (level: string) => {
|
||||
switch (level) {
|
||||
case 'success': return '✅';
|
||||
case 'warning': return '⚠️';
|
||||
case 'error': return '❌';
|
||||
default: return 'ℹ️';
|
||||
}
|
||||
};
|
||||
|
||||
const getLogColor = (level: string) => {
|
||||
switch (level) {
|
||||
case 'success': return 'text-[var(--color-success)]';
|
||||
case 'warning': return 'text-[var(--color-warning)]';
|
||||
case 'error': return 'text-[var(--color-error)]';
|
||||
default: return 'text-[var(--text-secondary)]';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-16 h-16 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Brain className="w-8 h-8 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
Tu asistente inteligente analizará tus datos históricos para ayudarte a tomar mejores decisiones de negocio
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Auto-start notification */}
|
||||
{data.autoStartTraining && trainingStatus === 'pending' && (
|
||||
<Card className="p-4 bg-[var(--color-info)]/10 border-[var(--color-info)]/20 mb-4">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<div className="animate-spin w-5 h-5 border-2 border-[var(--color-info)] border-t-transparent rounded-full"></div>
|
||||
<p className="text-[var(--color-info)] font-medium">
|
||||
Creando tu asistente inteligente automáticamente...
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Training Controls */}
|
||||
{trainingStatus === 'pending' && !data.autoStartTraining && (
|
||||
<Card className="p-6 text-center">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
|
||||
Crear tu Asistente Inteligente
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] mb-6">
|
||||
Analizaremos tus datos de ventas y productos para crear un asistente que te ayude a predecir demanda y optimizar tu inventario.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<Activity className="w-6 h-6 text-[var(--color-info)] mx-auto mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)]">Predicción de Ventas</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Anticipa cuánto vas a vender</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<TrendingUp className="w-6 h-6 text-[var(--color-success)] mx-auto mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)]">Recomendaciones</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Cuánto producir y comprar</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<Zap className="w-6 h-6 text-[var(--color-warning)] mx-auto mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)]">Alertas Automáticas</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Te avisa cuando reordenar</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--color-info)]/10 border border-[var(--color-info)]/20 rounded-lg p-4 mb-6">
|
||||
<h4 className="font-medium text-[var(--color-info)] mb-2">Información que analizaremos:</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-[var(--color-info)]">
|
||||
<div>
|
||||
<span className="font-medium">Productos:</span>
|
||||
<br />
|
||||
{(() => {
|
||||
const allStepData = data.allStepData || {};
|
||||
const detectedProducts = allStepData.review?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
|
||||
allStepData['data-processing']?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
|
||||
data.detectedProducts?.filter((p: any) => p.status === 'approved') || [];
|
||||
return detectedProducts.length || 3; // fallback to mock data count
|
||||
})()}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Inventario:</span>
|
||||
<br />
|
||||
{(() => {
|
||||
const allStepData = data.allStepData || {};
|
||||
const inventoryData = allStepData.inventory?.inventoryItems ||
|
||||
allStepData['inventory-setup']?.inventoryItems ||
|
||||
allStepData['inventory']?.inventoryItems ||
|
||||
data.inventoryItems || [];
|
||||
return inventoryData.length || 3; // fallback to mock data count
|
||||
})()} elementos
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Registros:</span>
|
||||
<br />
|
||||
{(() => {
|
||||
const allStepData = data.allStepData || {};
|
||||
const validation = allStepData['data-processing']?.validation ||
|
||||
allStepData.review?.validation ||
|
||||
data.validation || {};
|
||||
return validation.total_records || 1500; // fallback to mock data
|
||||
})()}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Período:</span>
|
||||
<br />
|
||||
{(() => {
|
||||
const allStepData = data.allStepData || {};
|
||||
const validation = allStepData['data-processing']?.validation ||
|
||||
allStepData.review?.validation ||
|
||||
data.validation || {};
|
||||
const dateRange = validation.summary?.date_range || '2024-01-01 to 2024-12-31';
|
||||
return dateRange.split(' to ').map((date: string) =>
|
||||
new Date(date).toLocaleDateString()
|
||||
).join(' - ');
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={startTraining} className="px-8 py-2">
|
||||
<Brain className="w-4 h-4 mr-2" />
|
||||
Crear mi Asistente Inteligente
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Training Progress */}
|
||||
{trainingStatus === 'training' && (
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<div className="text-center mb-6">
|
||||
<div className="animate-pulse w-12 h-12 bg-[var(--color-primary)] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Brain className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
Creando tu Asistente Inteligente...
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)]">{currentPhase}</p>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
Progreso de Creación
|
||||
</span>
|
||||
<div className="text-right">
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
{progress.toFixed(0)}%
|
||||
</span>
|
||||
{productsTotal > 0 && (
|
||||
<div className="text-xs text-[var(--text-tertiary)]">
|
||||
{productsCompleted}/{productsTotal} productos
|
||||
</div>
|
||||
)}
|
||||
{estimatedTimeRemaining !== null && estimatedTimeRemaining > 0 && (
|
||||
<div className="text-xs text-[var(--text-tertiary)]">
|
||||
~{estimatedTimeRemaining} min restantes
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-4">
|
||||
<div
|
||||
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-info)] h-4 rounded-full transition-all duration-1000 relative overflow-hidden"
|
||||
style={{ width: `${progress}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Metrics */}
|
||||
{metrics && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-xl font-bold text-[var(--color-success)]">
|
||||
{(metrics.accuracy * 100).toFixed(1)}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Precisión</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-xl font-bold text-[var(--color-info)]">
|
||||
{metrics.training_loss.toFixed(3)}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Loss</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-xl font-bold text-[var(--color-primary)]">
|
||||
{metrics.epochs_completed}/{metrics.total_epochs}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Epochs</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-xl font-bold text-[var(--color-secondary)]">
|
||||
{(metrics.f1_score * 100).toFixed(1)}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">F1-Score</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Training Logs */}
|
||||
<Card className="p-6">
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-4">Progreso del Asistente</h4>
|
||||
<div className="bg-black rounded-lg p-4 max-h-48 overflow-y-auto font-mono text-sm">
|
||||
{logs.slice(-10).map((log, index) => (
|
||||
<div key={index} className={`mb-1 ${getLogColor(log.level)}`}>
|
||||
<span className="text-[var(--text-quaternary)]">[{log.timestamp}]</span>
|
||||
<span className="ml-2">{getLogIcon(log.level)} {log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Training Completed */}
|
||||
{trainingStatus === 'completed' && finalResults && (
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-12 h-12 bg-[var(--color-success)] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-[var(--color-success)] mb-2">
|
||||
¡Tu Asistente Inteligente está Listo!
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
Tu asistente personalizado está listo para ayudarte con predicciones y recomendaciones
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Final Metrics */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
|
||||
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-success)]">
|
||||
{(finalResults.finalMetrics.accuracy * 100).toFixed(1)}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Exactitud</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--color-info)]/10 rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-info)]">
|
||||
{(finalResults.finalMetrics.precision * 100).toFixed(1)}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Confiabilidad</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--color-primary)]/10 rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-primary)]">
|
||||
{(finalResults.finalMetrics.recall * 100).toFixed(1)}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Cobertura</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--color-secondary-100)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-secondary)]">
|
||||
{(finalResults.finalMetrics.f1_score * 100).toFixed(1)}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Rendimiento</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--color-warning-100)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-warning)]">
|
||||
{finalResults.finalMetrics.total_epochs}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Iteraciones</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Info */}
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 mb-6">
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-3">Información del Modelo:</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text-tertiary)]">ID del Modelo:</span>
|
||||
<p className="font-mono text-[var(--text-primary)]">{finalResults.modelId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text-tertiary)]">Versión:</span>
|
||||
<p className="font-mono text-[var(--text-primary)]">{finalResults.modelVersion}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text-tertiary)]">Duración:</span>
|
||||
<p className="text-[var(--text-primary)]">{finalResults.trainingDuration}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text-tertiary)]">Dataset:</span>
|
||||
<p className="text-[var(--text-primary)]">{finalResults.datasetSize}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text-tertiary)]">Endpoint:</span>
|
||||
<p className="font-mono text-[var(--text-primary)]">{finalResults.deploymentUrl}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text-tertiary)]">Estado:</span>
|
||||
<p className="text-[var(--color-success)] font-medium">✓ Activo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Capabilities */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-4 border border-[var(--color-success)]/20 rounded-lg">
|
||||
<TrendingUp className="w-6 h-6 text-[var(--color-success)] mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-1">Predicción de Ventas</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Te dice cuánto vas a vender cada día
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border border-[var(--color-info)]/20 rounded-lg">
|
||||
<Activity className="w-6 h-6 text-[var(--color-info)] mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-1">Recomendaciones</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Te sugiere cuánto producir y comprar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border border-[var(--color-warning)]/20 rounded-lg">
|
||||
<Zap className="w-6 h-6 text-[var(--color-warning)] mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-1">Alertas Automáticas</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Te avisa cuando necesitas reordenar
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Training Error */}
|
||||
{trainingStatus === 'error' && (
|
||||
<Card className="p-6">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-[var(--color-error)] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<AlertCircle className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-[var(--color-error)] mb-2">
|
||||
Error al Crear el Asistente
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] mb-4">
|
||||
Ocurrió un problema durante la creación de tu asistente inteligente. Por favor, intenta nuevamente.
|
||||
</p>
|
||||
<Button onClick={retryTraining} variant="outline">
|
||||
Intentar Nuevamente
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Information */}
|
||||
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
||||
<h4 className="font-medium text-[var(--color-info)] mb-2">
|
||||
🎯 ¿Cómo funciona tu Asistente Inteligente?
|
||||
</h4>
|
||||
<ul className="text-sm text-[var(--color-info)] space-y-1">
|
||||
<li>• <strong>Analiza tus ventas:</strong> Estudia tus datos históricos para encontrar patrones</li>
|
||||
<li>• <strong>Entiende tu negocio:</strong> Aprende sobre temporadas altas y bajas</li>
|
||||
<li>• <strong>Hace predicciones:</strong> Te dice cuánto vas a vender cada día</li>
|
||||
<li>• <strong>Da recomendaciones:</strong> Te sugiere cuánto producir y comprar</li>
|
||||
<li>• <strong>Mejora con el tiempo:</strong> Se hace más inteligente con cada venta nueva</li>
|
||||
</ul>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
285
frontend/src/components/domain/onboarding/steps/ReviewStep.tsx
Normal file
285
frontend/src/components/domain/onboarding/steps/ReviewStep.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Eye, CheckCircle, AlertCircle, Edit, Trash2 } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../ui';
|
||||
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||
|
||||
interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
confidence: number;
|
||||
sales_count: number;
|
||||
estimated_price: number;
|
||||
status: 'approved' | 'rejected' | 'pending';
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
// Mock detected products
|
||||
const mockDetectedProducts: Product[] = [
|
||||
{ id: '1', name: 'Pan Integral', category: 'Panadería', confidence: 95, sales_count: 45, estimated_price: 2.50, status: 'pending' },
|
||||
{ id: '2', name: 'Croissant', category: 'Bollería', confidence: 92, sales_count: 38, estimated_price: 1.80, status: 'pending' },
|
||||
{ id: '3', name: 'Baguette', category: 'Panadería', confidence: 88, sales_count: 22, estimated_price: 3.00, status: 'pending' },
|
||||
{ id: '4', name: 'Empanada de Pollo', category: 'Salados', confidence: 85, sales_count: 31, estimated_price: 4.50, status: 'pending' },
|
||||
{ id: '5', name: 'Tarta de Manzana', category: 'Repostería', confidence: 78, sales_count: 12, estimated_price: 15.00, status: 'pending' },
|
||||
{ id: '6', name: 'Pan de Centeno', category: 'Panadería', confidence: 91, sales_count: 18, estimated_price: 2.80, status: 'pending' },
|
||||
{ id: '7', name: 'Medialunas', category: 'Bollería', confidence: 87, sales_count: 29, estimated_price: 1.20, status: 'pending' },
|
||||
{ id: '8', name: 'Sandwich Mixto', category: 'Salados', confidence: 82, sales_count: 25, estimated_price: 5.50, status: 'pending' }
|
||||
];
|
||||
|
||||
export const ReviewStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
onNext,
|
||||
onPrevious,
|
||||
isFirstStep,
|
||||
isLastStep
|
||||
}) => {
|
||||
// Generate products from processing results or use mock data
|
||||
const generateProductsFromResults = (results: any) => {
|
||||
if (!results?.product_list) return mockDetectedProducts;
|
||||
|
||||
return results.product_list.map((name: string, index: number) => ({
|
||||
id: (index + 1).toString(),
|
||||
name,
|
||||
category: index < 3 ? 'Panadería' : index < 5 ? 'Bollería' : 'Salados',
|
||||
confidence: Math.max(75, results.confidenceScore - Math.random() * 15),
|
||||
sales_count: Math.floor(Math.random() * 50) + 10,
|
||||
estimated_price: Math.random() * 5 + 1.5,
|
||||
status: 'pending' as const
|
||||
}));
|
||||
};
|
||||
|
||||
const [products, setProducts] = useState<Product[]>(
|
||||
data.detectedProducts || generateProductsFromResults(data.processingResults)
|
||||
);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
||||
|
||||
const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))];
|
||||
|
||||
useEffect(() => {
|
||||
onDataChange({
|
||||
...data,
|
||||
detectedProducts: products,
|
||||
reviewCompleted: products.every(p => p.status !== 'pending')
|
||||
});
|
||||
}, [products]);
|
||||
|
||||
const handleProductAction = (productId: string, action: 'approve' | 'reject') => {
|
||||
setProducts(prev => prev.map(product =>
|
||||
product.id === productId
|
||||
? { ...product, status: action === 'approve' ? 'approved' : 'rejected' }
|
||||
: product
|
||||
));
|
||||
};
|
||||
|
||||
const handleBulkAction = (action: 'approve' | 'reject') => {
|
||||
const filteredProducts = getFilteredProducts();
|
||||
setProducts(prev => prev.map(product =>
|
||||
filteredProducts.some(fp => fp.id === product.id)
|
||||
? { ...product, status: action === 'approve' ? 'approved' : 'rejected' }
|
||||
: product
|
||||
));
|
||||
};
|
||||
|
||||
const getFilteredProducts = () => {
|
||||
if (selectedCategory === 'all') {
|
||||
return products;
|
||||
}
|
||||
return products.filter(p => p.category === selectedCategory);
|
||||
};
|
||||
|
||||
const stats = {
|
||||
total: products.length,
|
||||
approved: products.filter(p => p.status === 'approved').length,
|
||||
rejected: products.filter(p => p.status === 'rejected').length,
|
||||
pending: products.filter(p => p.status === 'pending').length
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.total}</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Total</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-success)]">{stats.approved}</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Aprobados</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--color-error)]/10 rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-error)]">{stats.rejected}</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Rechazados</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--color-warning)]/10 rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-warning)]">{stats.pending}</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Pendientes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters and Actions */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-2">
|
||||
Filtrar por categoría:
|
||||
</label>
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="border border-[var(--border-secondary)] rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
|
||||
>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat === 'all' ? 'Todas las categorías' : cat}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleBulkAction('approve')}
|
||||
className="text-[var(--color-success)] border-[var(--color-success)] hover:bg-[var(--color-success)]/10"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-1" />
|
||||
Aprobar Visibles
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleBulkAction('reject')}
|
||||
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
Rechazar Visibles
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Products List */}
|
||||
<div className="space-y-4">
|
||||
{getFilteredProducts().map((product) => (
|
||||
<Card key={product.id} className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-4 flex-1">
|
||||
{/* Status Icon */}
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center mt-1 ${
|
||||
product.status === 'approved'
|
||||
? 'bg-[var(--color-success)]'
|
||||
: product.status === 'rejected'
|
||||
? 'bg-[var(--color-error)]'
|
||||
: 'bg-[var(--bg-secondary)] border border-[var(--border-secondary)]'
|
||||
}`}>
|
||||
{product.status === 'approved' ? (
|
||||
<CheckCircle className="w-4 h-4 text-white" />
|
||||
) : product.status === 'rejected' ? (
|
||||
<Trash2 className="w-4 h-4 text-white" />
|
||||
) : (
|
||||
<Eye className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{product.name}</h4>
|
||||
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Badge variant="gray">{product.category}</Badge>
|
||||
{product.status !== 'pending' && (
|
||||
<Badge variant={product.status === 'approved' ? 'green' : 'red'}>
|
||||
{product.status === 'approved' ? 'Aprobado' : 'Rechazado'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
product.confidence >= 90
|
||||
? 'bg-[var(--color-success)]/10 text-[var(--color-success)]'
|
||||
: product.confidence >= 75
|
||||
? 'bg-[var(--color-warning)]/10 text-[var(--color-warning)]'
|
||||
: 'bg-[var(--color-error)]/10 text-[var(--color-error)]'
|
||||
}`}>
|
||||
{product.confidence}% confianza
|
||||
</span>
|
||||
<span>{product.sales_count} ventas</span>
|
||||
<span>${product.estimated_price.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 ml-4 mt-1">
|
||||
{product.status === 'pending' ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleProductAction(product.id, 'approve')}
|
||||
className="bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-1" />
|
||||
Aprobar
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleProductAction(product.id, 'reject')}
|
||||
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
Rechazar
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setProducts(prev => prev.map(p => p.id === product.id ? {...p, status: 'pending'} : p))}
|
||||
className="text-[var(--text-secondary)] hover:text-[var(--color-primary)]"
|
||||
>
|
||||
<Edit className="w-4 h-4 mr-1" />
|
||||
Modificar
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Progress Indicator */}
|
||||
{stats.pending > 0 && (
|
||||
<Card className="p-4 bg-[var(--color-warning)]/5 border-[var(--color-warning)]/20">
|
||||
<div className="flex items-center space-x-3">
|
||||
<AlertCircle className="w-5 h-5 text-[var(--color-warning)]" />
|
||||
<div>
|
||||
<p className="font-medium text-[var(--text-primary)]">
|
||||
{stats.pending} productos pendientes de revisión
|
||||
</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Revisa todos los productos antes de continuar al siguiente paso
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Help Information */}
|
||||
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
||||
<h4 className="font-medium text-[var(--color-info)] mb-3">
|
||||
💡 Consejos para la revisión:
|
||||
</h4>
|
||||
<ul className="text-sm text-[var(--color-info)] space-y-1">
|
||||
<li>• <strong>Confianza alta (90%+):</strong> Productos identificados con alta precisión</li>
|
||||
<li>• <strong>Confianza media (75-89%):</strong> Revisar nombres y categorías</li>
|
||||
<li>• <strong>Confianza baja (<75%):</strong> Verificar que corresponden a tu catálogo</li>
|
||||
<li>• Usa las acciones masivas para aprobar/rechazar por categoría completa</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,585 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Truck, Phone, Mail, Plus, Edit, Trash2, MapPin } from 'lucide-react';
|
||||
import { Button, Card, Input, Badge } from '../../../ui';
|
||||
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||
|
||||
interface Supplier {
|
||||
id: string;
|
||||
name: string;
|
||||
contact_person: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
categories: string[];
|
||||
payment_terms: string;
|
||||
delivery_days: string[];
|
||||
min_order_amount?: number;
|
||||
notes?: string;
|
||||
status: 'active' | 'inactive';
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Mock suppliers
|
||||
const mockSuppliers: Supplier[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Molinos del Sur',
|
||||
contact_person: 'Juan Pérez',
|
||||
phone: '+1 555-0123',
|
||||
email: 'ventas@molinosdelsur.com',
|
||||
address: 'Av. Industrial 123, Zona Sur',
|
||||
categories: ['Harinas', 'Granos'],
|
||||
payment_terms: '30 días',
|
||||
delivery_days: ['Lunes', 'Miércoles', 'Viernes'],
|
||||
min_order_amount: 200,
|
||||
notes: 'Proveedor principal de harinas, muy confiable',
|
||||
status: 'active',
|
||||
created_at: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Lácteos Premium',
|
||||
contact_person: 'María González',
|
||||
phone: '+1 555-0456',
|
||||
email: 'pedidos@lacteospremium.com',
|
||||
address: 'Calle Central 456, Centro',
|
||||
categories: ['Lácteos', 'Mantequillas', 'Quesos'],
|
||||
payment_terms: '15 días',
|
||||
delivery_days: ['Martes', 'Jueves', 'Sábado'],
|
||||
min_order_amount: 150,
|
||||
status: 'active',
|
||||
created_at: '2024-01-20'
|
||||
}
|
||||
];
|
||||
|
||||
const daysOfWeek = [
|
||||
'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'
|
||||
];
|
||||
|
||||
const commonCategories = [
|
||||
'Harinas', 'Lácteos', 'Levaduras', 'Azúcares', 'Grasas', 'Huevos',
|
||||
'Frutas', 'Chocolates', 'Frutos Secos', 'Especias', 'Conservantes'
|
||||
];
|
||||
|
||||
export const SuppliersStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
onNext,
|
||||
onPrevious,
|
||||
isFirstStep,
|
||||
isLastStep
|
||||
}) => {
|
||||
const [suppliers, setSuppliers] = useState<Supplier[]>(
|
||||
data.suppliers || mockSuppliers
|
||||
);
|
||||
const [editingSupplier, setEditingSupplier] = useState<Supplier | null>(null);
|
||||
const [isAddingNew, setIsAddingNew] = useState(false);
|
||||
const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'inactive'>('all');
|
||||
|
||||
useEffect(() => {
|
||||
onDataChange({
|
||||
...data,
|
||||
suppliers: suppliers,
|
||||
suppliersConfigured: true // This step is optional
|
||||
});
|
||||
}, [suppliers]);
|
||||
|
||||
const handleAddSupplier = () => {
|
||||
const newSupplier: Supplier = {
|
||||
id: Date.now().toString(),
|
||||
name: '',
|
||||
contact_person: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
categories: [],
|
||||
payment_terms: '30 días',
|
||||
delivery_days: [],
|
||||
status: 'active',
|
||||
created_at: new Date().toISOString().split('T')[0]
|
||||
};
|
||||
setEditingSupplier(newSupplier);
|
||||
setIsAddingNew(true);
|
||||
};
|
||||
|
||||
const handleSaveSupplier = (supplier: Supplier) => {
|
||||
if (isAddingNew) {
|
||||
setSuppliers(prev => [...prev, supplier]);
|
||||
} else {
|
||||
setSuppliers(prev => prev.map(s => s.id === supplier.id ? supplier : s));
|
||||
}
|
||||
setEditingSupplier(null);
|
||||
setIsAddingNew(false);
|
||||
};
|
||||
|
||||
const handleDeleteSupplier = (id: string) => {
|
||||
if (window.confirm('¿Estás seguro de eliminar este proveedor?')) {
|
||||
setSuppliers(prev => prev.filter(s => s.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSupplierStatus = (id: string) => {
|
||||
setSuppliers(prev => prev.map(s =>
|
||||
s.id === id
|
||||
? { ...s, status: s.status === 'active' ? 'inactive' : 'active' }
|
||||
: s
|
||||
));
|
||||
};
|
||||
|
||||
const getFilteredSuppliers = () => {
|
||||
return filterStatus === 'all'
|
||||
? suppliers
|
||||
: suppliers.filter(s => s.status === filterStatus);
|
||||
};
|
||||
|
||||
const stats = {
|
||||
total: suppliers.length,
|
||||
active: suppliers.filter(s => s.status === 'active').length,
|
||||
inactive: suppliers.filter(s => s.status === 'inactive').length,
|
||||
categories: Array.from(new Set(suppliers.flatMap(s => s.categories))).length
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Optional Step Notice */}
|
||||
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
||||
<p className="text-sm text-[var(--color-info)]">
|
||||
💡 <strong>Paso Opcional:</strong> Puedes saltar este paso y configurar proveedores más tarde desde el módulo de compras
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{stats.total} proveedores configurados ({stats.active} activos)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleAddSupplier}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Agregar Proveedor
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Card className="p-4 text-center">
|
||||
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.total}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Total</p>
|
||||
</Card>
|
||||
<Card className="p-4 text-center">
|
||||
<p className="text-2xl font-bold text-[var(--color-success)]">{stats.active}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Activos</p>
|
||||
</Card>
|
||||
<Card className="p-4 text-center">
|
||||
<p className="text-2xl font-bold text-[var(--text-tertiary)]">{stats.inactive}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Inactivos</p>
|
||||
</Card>
|
||||
<Card className="p-4 text-center">
|
||||
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.categories}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Categorías</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">Estado:</label>
|
||||
<div className="flex space-x-2">
|
||||
{[
|
||||
{ value: 'all', label: 'Todos' },
|
||||
{ value: 'active', label: 'Activos' },
|
||||
{ value: 'inactive', label: 'Inactivos' }
|
||||
].map(filter => (
|
||||
<button
|
||||
key={filter.value}
|
||||
onClick={() => setFilterStatus(filter.value as any)}
|
||||
className={`px-3 py-1 text-sm rounded-full transition-colors ${
|
||||
filterStatus === filter.value
|
||||
? 'bg-[var(--color-primary)] text-white'
|
||||
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
|
||||
}`}
|
||||
>
|
||||
{filter.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Suppliers List */}
|
||||
<div className="space-y-4">
|
||||
{getFilteredSuppliers().map((supplier) => (
|
||||
<Card key={supplier.id} className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-4 flex-1">
|
||||
{/* Status Icon */}
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center mt-1 ${
|
||||
supplier.status === 'active'
|
||||
? 'bg-[var(--color-success)]/10'
|
||||
: 'bg-[var(--bg-secondary)] border border-[var(--border-secondary)]'
|
||||
}`}>
|
||||
<Truck className={`w-4 h-4 ${
|
||||
supplier.status === 'active'
|
||||
? 'text-[var(--color-success)]'
|
||||
: 'text-[var(--text-tertiary)]'
|
||||
}`} />
|
||||
</div>
|
||||
|
||||
{/* Supplier Info */}
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{supplier.name}</h4>
|
||||
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Badge variant={supplier.status === 'active' ? 'green' : 'gray'}>
|
||||
{supplier.status === 'active' ? 'Activo' : 'Inactivo'}
|
||||
</Badge>
|
||||
{supplier.categories.slice(0, 2).map((cat, idx) => (
|
||||
<Badge key={idx} variant="blue">{cat}</Badge>
|
||||
))}
|
||||
{supplier.categories.length > 2 && (
|
||||
<Badge variant="gray">+{supplier.categories.length - 2}</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 text-sm text-[var(--text-secondary)] mb-2">
|
||||
<div>
|
||||
<span className="text-[var(--text-tertiary)]">Contacto: </span>
|
||||
<span className="font-medium text-[var(--text-primary)]">{supplier.contact_person}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-[var(--text-tertiary)]">Entrega: </span>
|
||||
<span className="font-medium text-[var(--text-primary)]">
|
||||
{supplier.delivery_days.slice(0, 2).join(', ')}
|
||||
{supplier.delivery_days.length > 2 && ` +${supplier.delivery_days.length - 2}`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-[var(--text-tertiary)]">Pago: </span>
|
||||
<span className="font-medium text-[var(--text-primary)]">{supplier.payment_terms}</span>
|
||||
</div>
|
||||
|
||||
{supplier.min_order_amount && (
|
||||
<div>
|
||||
<span className="text-[var(--text-tertiary)]">Mín: </span>
|
||||
<span className="font-medium text-[var(--text-primary)]">${supplier.min_order_amount}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
|
||||
<div className="flex items-center gap-1">
|
||||
<Phone className="w-3 h-3" />
|
||||
<span>{supplier.phone}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Mail className="w-3 h-3" />
|
||||
<span>{supplier.email}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<MapPin className="w-3 h-3" />
|
||||
<span>{supplier.address}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{supplier.notes && (
|
||||
<div className="mt-3 p-2 bg-[var(--bg-secondary)] rounded text-sm">
|
||||
<span className="text-[var(--text-tertiary)]">Notas: </span>
|
||||
<span className="text-[var(--text-primary)]">{supplier.notes}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 ml-4 mt-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setEditingSupplier(supplier)}
|
||||
>
|
||||
<Edit className="w-4 h-4 mr-1" />
|
||||
Editar
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => toggleSupplierStatus(supplier.id)}
|
||||
className={supplier.status === 'active'
|
||||
? 'text-[var(--color-warning)] border-[var(--color-warning)] hover:bg-[var(--color-warning)]/10'
|
||||
: 'text-[var(--color-success)] border-[var(--color-success)] hover:bg-[var(--color-success)]/10'
|
||||
}
|
||||
>
|
||||
{supplier.status === 'active' ? 'Pausar' : 'Activar'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleDeleteSupplier(supplier.id)}
|
||||
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{getFilteredSuppliers().length === 0 && (
|
||||
<Card className="p-8 text-center">
|
||||
<Truck className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
|
||||
<p className="text-[var(--text-secondary)] mb-4">No hay proveedores en esta categoría</p>
|
||||
<Button onClick={handleAddSupplier}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Agregar Primer Proveedor
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Edit Modal */}
|
||||
{editingSupplier && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<Card className="p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
|
||||
{isAddingNew ? 'Agregar Proveedor' : 'Editar Proveedor'}
|
||||
</h3>
|
||||
|
||||
<SupplierForm
|
||||
supplier={editingSupplier}
|
||||
onSave={handleSaveSupplier}
|
||||
onCancel={() => {
|
||||
setEditingSupplier(null);
|
||||
setIsAddingNew(false);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Information */}
|
||||
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
||||
<h4 className="font-medium text-[var(--color-info)] mb-2">
|
||||
🚚 Gestión de Proveedores:
|
||||
</h4>
|
||||
<ul className="text-sm text-[var(--color-info)] space-y-1">
|
||||
<li>• <strong>Este paso es opcional</strong> - puedes configurar proveedores más tarde</li>
|
||||
<li>• Define categorías de productos para facilitar la búsqueda</li>
|
||||
<li>• Establece días de entrega y términos de pago</li>
|
||||
<li>• Configura montos mínimos de pedido para optimizar compras</li>
|
||||
</ul>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Component for editing suppliers
|
||||
interface SupplierFormProps {
|
||||
supplier: Supplier;
|
||||
onSave: (supplier: Supplier) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const SupplierForm: React.FC<SupplierFormProps> = ({ supplier, onSave, onCancel }) => {
|
||||
const [formData, setFormData] = useState(supplier);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.name.trim()) {
|
||||
alert('El nombre es requerido');
|
||||
return;
|
||||
}
|
||||
if (!formData.contact_person.trim()) {
|
||||
alert('El contacto es requerido');
|
||||
return;
|
||||
}
|
||||
onSave(formData);
|
||||
};
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
categories: prev.categories.includes(category)
|
||||
? prev.categories.filter(c => c !== category)
|
||||
: [...prev.categories, category]
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleDeliveryDay = (day: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
delivery_days: prev.delivery_days.includes(day)
|
||||
? prev.delivery_days.filter(d => d !== day)
|
||||
: [...prev.delivery_days, day]
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Nombre de la Empresa *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="Molinos del Sur"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Persona de Contacto *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.contact_person}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, contact_person: e.target.value }))}
|
||||
placeholder="Juan Pérez"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Teléfono
|
||||
</label>
|
||||
<Input
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, phone: e.target.value }))}
|
||||
placeholder="+1 555-0123"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
|
||||
placeholder="ventas@proveedor.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Dirección
|
||||
</label>
|
||||
<Input
|
||||
value={formData.address}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, address: e.target.value }))}
|
||||
placeholder="Av. Industrial 123, Zona Sur"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
Categorías de Productos
|
||||
</label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{commonCategories.map(category => (
|
||||
<label key={category} className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.categories.includes(category)}
|
||||
onChange={() => toggleCategory(category)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm text-[var(--text-primary)]">{category}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Términos de Pago
|
||||
</label>
|
||||
<select
|
||||
value={formData.payment_terms}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, payment_terms: e.target.value }))}
|
||||
className="w-full p-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||
>
|
||||
<option value="Inmediato">Inmediato</option>
|
||||
<option value="15 días">15 días</option>
|
||||
<option value="30 días">30 días</option>
|
||||
<option value="45 días">45 días</option>
|
||||
<option value="60 días">60 días</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Pedido Mínimo
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.min_order_amount || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, min_order_amount: Number(e.target.value) }))}
|
||||
placeholder="200"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
Días de Entrega
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{daysOfWeek.map(day => (
|
||||
<label key={day} className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.delivery_days.includes(day)}
|
||||
onChange={() => toggleDeliveryDay(day)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm text-[var(--text-primary)]">{day}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Notas
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.notes || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, notes: e.target.value }))}
|
||||
placeholder="Información adicional sobre el proveedor..."
|
||||
className="w-full p-2 border border-[var(--border-secondary)] rounded resize-none focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4 border-t">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
Guardar Proveedor
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
40
frontend/src/components/shared/LoadingSpinner.tsx
Normal file
40
frontend/src/components/shared/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
overlay?: boolean;
|
||||
text?: string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
||||
overlay = false,
|
||||
text,
|
||||
size = 'md'
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-8 h-8',
|
||||
lg: 'w-12 h-12'
|
||||
};
|
||||
|
||||
const spinner = (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className={`animate-spin rounded-full border-4 border-[var(--border-secondary)] border-t-[var(--color-primary)] ${sizeClasses[size]}`}></div>
|
||||
{text && (
|
||||
<p className="mt-4 text-[var(--text-secondary)] text-sm">{text}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (overlay) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-[var(--bg-primary)] rounded-lg p-6">
|
||||
{spinner}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return spinner;
|
||||
};
|
||||
18
frontend/src/hooks/useAuth.ts
Normal file
18
frontend/src/hooks/useAuth.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// Mock auth hook for testing
|
||||
export const useAuth = () => {
|
||||
return {
|
||||
user: {
|
||||
id: 'user_123',
|
||||
tenant_id: 'tenant_456',
|
||||
email: 'user@example.com',
|
||||
name: 'Usuario Demo'
|
||||
},
|
||||
isAuthenticated: true,
|
||||
login: async (credentials: any) => {
|
||||
console.log('Mock login:', credentials);
|
||||
},
|
||||
logout: () => {
|
||||
console.log('Mock logout');
|
||||
}
|
||||
};
|
||||
};
|
||||
161
frontend/src/pages/app/onboarding/OnboardingPage.tsx
Normal file
161
frontend/src/pages/app/onboarding/OnboardingPage.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { OnboardingWizard, OnboardingStep } from '../../../components/domain/onboarding/OnboardingWizard';
|
||||
import { onboardingApiService } from '../../../services/api/onboarding.service';
|
||||
import { useAuth } from '../../../hooks/useAuth';
|
||||
import { LoadingSpinner } from '../../../components/shared/LoadingSpinner';
|
||||
|
||||
// Step Components
|
||||
import { BakerySetupStep } from '../../../components/domain/onboarding/steps/BakerySetupStep';
|
||||
import { DataProcessingStep } from '../../../components/domain/onboarding/steps/DataProcessingStep';
|
||||
import { ReviewStep } from '../../../components/domain/onboarding/steps/ReviewStep';
|
||||
import { InventorySetupStep } from '../../../components/domain/onboarding/steps/InventorySetupStep';
|
||||
import { SuppliersStep } from '../../../components/domain/onboarding/steps/SuppliersStep';
|
||||
import { MLTrainingStep } from '../../../components/domain/onboarding/steps/MLTrainingStep';
|
||||
import { CompletionStep } from '../../../components/domain/onboarding/steps/CompletionStep';
|
||||
|
||||
const OnboardingPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [globalData, setGlobalData] = useState<any>({});
|
||||
|
||||
// Define the 8 onboarding steps (simplified by merging data upload + analysis)
|
||||
const steps: OnboardingStep[] = [
|
||||
{
|
||||
id: 'setup',
|
||||
title: '🏢 Setup',
|
||||
description: 'Configuración básica de tu panadería y creación del tenant',
|
||||
component: BakerySetupStep,
|
||||
isRequired: true,
|
||||
validation: (data) => {
|
||||
if (!data.bakery?.name) return 'El nombre de la panadería es requerido';
|
||||
if (!data.bakery?.type) return 'El tipo de panadería es requerido';
|
||||
if (!data.bakery?.location) return 'La ubicación es requerida';
|
||||
// Tenant creation will happen automatically when validation passes
|
||||
return null;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'data-processing',
|
||||
title: '📊 Historial de Ventas',
|
||||
description: 'Sube tus datos de ventas para obtener insights personalizados',
|
||||
component: DataProcessingStep,
|
||||
isRequired: true,
|
||||
validation: (data) => {
|
||||
if (!data.files?.salesData) return 'Debes cargar el archivo de datos de ventas';
|
||||
if (data.processingStage !== 'completed') return 'El procesamiento debe completarse antes de continuar';
|
||||
if (!data.processingResults?.is_valid) return 'Los datos deben ser válidos para continuar';
|
||||
return null;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'review',
|
||||
title: '📋 Revisión',
|
||||
description: 'Revisión de productos detectados por IA y resultados',
|
||||
component: ReviewStep,
|
||||
isRequired: true,
|
||||
validation: (data) => {
|
||||
if (!data.reviewCompleted) return 'Debes revisar y aprobar los productos detectados';
|
||||
return null;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'inventory',
|
||||
title: '⚙️ Inventario',
|
||||
description: 'Configuración de inventario (stock, fechas de vencimiento)',
|
||||
component: InventorySetupStep,
|
||||
isRequired: true,
|
||||
validation: (data) => {
|
||||
if (!data.inventoryConfigured) return 'Debes configurar el inventario básico';
|
||||
return null;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'suppliers',
|
||||
title: '🏪 Proveedores',
|
||||
description: 'Configuración de proveedores y asociaciones',
|
||||
component: SuppliersStep,
|
||||
isRequired: false,
|
||||
validation: () => null // Optional step
|
||||
},
|
||||
{
|
||||
id: 'ml-training',
|
||||
title: '🎯 Inteligencia',
|
||||
description: 'Creación de tu asistente inteligente personalizado',
|
||||
component: MLTrainingStep,
|
||||
isRequired: true,
|
||||
validation: (data) => {
|
||||
if (data.trainingStatus !== 'completed') return 'El entrenamiento del modelo debe completarse';
|
||||
return null;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'completion',
|
||||
title: '🎉 Listo',
|
||||
description: 'Finalización y preparación para usar la plataforma',
|
||||
component: CompletionStep,
|
||||
isRequired: true,
|
||||
validation: () => null
|
||||
}
|
||||
];
|
||||
|
||||
const handleComplete = async (allData: any) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Mark onboarding as complete in the backend
|
||||
if (user?.tenant_id) {
|
||||
await onboardingApiService.completeOnboarding(user.tenant_id, {
|
||||
completedAt: new Date().toISOString(),
|
||||
data: allData
|
||||
});
|
||||
}
|
||||
|
||||
// Navigate to dashboard
|
||||
navigate('/app/dashboard', {
|
||||
state: {
|
||||
message: '¡Felicidades! Tu panadería ha sido configurada exitosamente.',
|
||||
type: 'success'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error completing onboarding:', error);
|
||||
// Still navigate to dashboard but show warning
|
||||
navigate('/app/dashboard', {
|
||||
state: {
|
||||
message: 'Configuración completada. Algunos ajustes finales pueden estar pendientes.',
|
||||
type: 'warning'
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExit = () => {
|
||||
const confirmExit = window.confirm(
|
||||
'¿Estás seguro de que quieres salir del proceso de configuración? Tu progreso se guardará automáticamente.'
|
||||
);
|
||||
|
||||
if (confirmExit) {
|
||||
navigate('/app/dashboard');
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner overlay text="Completando configuración..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--bg-primary)]">
|
||||
<OnboardingWizard
|
||||
steps={steps}
|
||||
onComplete={handleComplete}
|
||||
onExit={handleExit}
|
||||
className="py-8"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingPage;
|
||||
@@ -1,451 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { BarChart3, TrendingUp, Target, AlertCircle, CheckCircle, Eye, Download } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const OnboardingAnalysisPage: React.FC = () => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('30days');
|
||||
|
||||
const analysisData = {
|
||||
onboardingScore: 87,
|
||||
completionRate: 92,
|
||||
averageTime: '4.2 días',
|
||||
stepsCompleted: 15,
|
||||
totalSteps: 16,
|
||||
dataQuality: 94
|
||||
};
|
||||
|
||||
const stepProgress = [
|
||||
{ step: 'Información Básica', completed: true, quality: 95, timeSpent: '25 min' },
|
||||
{ step: 'Configuración de Menú', completed: true, quality: 88, timeSpent: '1.2 horas' },
|
||||
{ step: 'Datos de Inventario', completed: true, quality: 92, timeSpent: '45 min' },
|
||||
{ step: 'Configuración de Horarios', completed: true, quality: 100, timeSpent: '15 min' },
|
||||
{ step: 'Integración de Pagos', completed: true, quality: 85, timeSpent: '30 min' },
|
||||
{ step: 'Carga de Productos', completed: true, quality: 90, timeSpent: '2.1 horas' },
|
||||
{ step: 'Configuración de Personal', completed: true, quality: 87, timeSpent: '40 min' },
|
||||
{ step: 'Pruebas del Sistema', completed: false, quality: 0, timeSpent: '-' }
|
||||
];
|
||||
|
||||
const insights = [
|
||||
{
|
||||
type: 'success',
|
||||
title: 'Excelente Progreso',
|
||||
description: 'Has completado el 94% del proceso de configuración inicial',
|
||||
recommendation: 'Solo faltan las pruebas finales del sistema para completar tu configuración',
|
||||
impact: 'high'
|
||||
},
|
||||
{
|
||||
type: 'info',
|
||||
title: 'Calidad de Datos Alta',
|
||||
description: 'Tus datos tienen una calidad promedio del 94%',
|
||||
recommendation: 'Considera revisar la configuración de pagos para mejorar la puntuación',
|
||||
impact: 'medium'
|
||||
},
|
||||
{
|
||||
type: 'warning',
|
||||
title: 'Paso Pendiente',
|
||||
description: 'Las pruebas del sistema están pendientes',
|
||||
recommendation: 'Programa las pruebas para validar la configuración completa',
|
||||
impact: 'high'
|
||||
}
|
||||
];
|
||||
|
||||
const dataAnalysis = [
|
||||
{
|
||||
category: 'Información del Negocio',
|
||||
completeness: 100,
|
||||
accuracy: 95,
|
||||
items: 12,
|
||||
issues: 0,
|
||||
details: 'Toda la información básica está completa y verificada'
|
||||
},
|
||||
{
|
||||
category: 'Menú y Productos',
|
||||
completeness: 85,
|
||||
accuracy: 88,
|
||||
items: 45,
|
||||
issues: 3,
|
||||
details: '3 productos sin precios definidos'
|
||||
},
|
||||
{
|
||||
category: 'Inventario Inicial',
|
||||
completeness: 92,
|
||||
accuracy: 90,
|
||||
items: 28,
|
||||
issues: 2,
|
||||
details: '2 ingredientes sin stock mínimo definido'
|
||||
},
|
||||
{
|
||||
category: 'Configuración Operativa',
|
||||
completeness: 100,
|
||||
accuracy: 100,
|
||||
items: 8,
|
||||
issues: 0,
|
||||
details: 'Horarios y políticas completamente configuradas'
|
||||
}
|
||||
];
|
||||
|
||||
const benchmarkComparison = {
|
||||
industry: {
|
||||
onboardingScore: 74,
|
||||
completionRate: 78,
|
||||
averageTime: '6.8 días'
|
||||
},
|
||||
yourData: {
|
||||
onboardingScore: 87,
|
||||
completionRate: 92,
|
||||
averageTime: '4.2 días'
|
||||
}
|
||||
};
|
||||
|
||||
const recommendations = [
|
||||
{
|
||||
priority: 'high',
|
||||
title: 'Completar Pruebas del Sistema',
|
||||
description: 'Realizar pruebas integrales para validar toda la configuración',
|
||||
estimatedTime: '30 minutos',
|
||||
impact: 'Garantiza funcionamiento óptimo del sistema'
|
||||
},
|
||||
{
|
||||
priority: 'medium',
|
||||
title: 'Revisar Precios de Productos',
|
||||
description: 'Definir precios para los 3 productos pendientes',
|
||||
estimatedTime: '15 minutos',
|
||||
impact: 'Permitirá generar ventas de todos los productos'
|
||||
},
|
||||
{
|
||||
priority: 'medium',
|
||||
title: 'Configurar Stocks Mínimos',
|
||||
description: 'Establecer niveles mínimos para 2 ingredientes',
|
||||
estimatedTime: '10 minutos',
|
||||
impact: 'Mejorará el control de inventario automático'
|
||||
},
|
||||
{
|
||||
priority: 'low',
|
||||
title: 'Optimizar Configuración de Pagos',
|
||||
description: 'Revisar métodos de pago y comisiones',
|
||||
estimatedTime: '20 minutos',
|
||||
impact: 'Puede reducir costos de transacción'
|
||||
}
|
||||
];
|
||||
|
||||
const getInsightIcon = (type: string) => {
|
||||
const iconProps = { className: "w-5 h-5" };
|
||||
switch (type) {
|
||||
case 'success': return <CheckCircle {...iconProps} className="w-5 h-5 text-[var(--color-success)]" />;
|
||||
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
|
||||
case 'info': return <Target {...iconProps} className="w-5 h-5 text-[var(--color-info)]" />;
|
||||
default: return <AlertCircle {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getInsightColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'success': return 'bg-green-50 border-green-200';
|
||||
case 'warning': return 'bg-yellow-50 border-yellow-200';
|
||||
case 'info': return 'bg-[var(--color-info)]/5 border-[var(--color-info)]/20';
|
||||
default: return 'bg-[var(--bg-secondary)] border-[var(--border-primary)]';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'red';
|
||||
case 'medium': return 'yellow';
|
||||
case 'low': return 'green';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getCompletionColor = (percentage: number) => {
|
||||
if (percentage >= 95) return 'text-[var(--color-success)]';
|
||||
if (percentage >= 80) return 'text-yellow-600';
|
||||
return 'text-[var(--color-error)]';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Análisis de Configuración"
|
||||
description="Análisis detallado de tu proceso de configuración y recomendaciones"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Ver Detalles
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar Reporte
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Overall Score */}
|
||||
<Card className="p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-6">
|
||||
<div className="text-center">
|
||||
<div className="relative w-24 h-24 mx-auto mb-3">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-2xl font-bold text-[var(--color-success)]">{analysisData.onboardingScore}</span>
|
||||
</div>
|
||||
<svg className="w-24 h-24 transform -rotate-90">
|
||||
<circle
|
||||
cx="48"
|
||||
cy="48"
|
||||
r="40"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
fill="none"
|
||||
className="text-gray-200"
|
||||
/>
|
||||
<circle
|
||||
cx="48"
|
||||
cy="48"
|
||||
r="40"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
fill="none"
|
||||
strokeDasharray={`${analysisData.onboardingScore * 2.51} 251`}
|
||||
className="text-[var(--color-success)]"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Puntuación General</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-[var(--color-info)]">{analysisData.completionRate}%</p>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Completado</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-purple-600">{analysisData.averageTime}</p>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Tiempo Promedio</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-[var(--color-primary)]">{analysisData.stepsCompleted}/{analysisData.totalSteps}</p>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Pasos Completados</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-teal-600">{analysisData.dataQuality}%</p>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Calidad de Datos</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center mb-2">
|
||||
<TrendingUp className="w-8 h-8 text-[var(--color-success)]" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Por encima del promedio</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Progress Analysis */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Progreso por Pasos</h3>
|
||||
<div className="space-y-4">
|
||||
{stepProgress.map((step, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
step.completed ? 'bg-[var(--color-success)]/10' : 'bg-[var(--bg-tertiary)]'
|
||||
}`}>
|
||||
{step.completed ? (
|
||||
<CheckCircle className="w-5 h-5 text-[var(--color-success)]" />
|
||||
) : (
|
||||
<span className="text-sm font-medium text-[var(--text-tertiary)]">{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-primary)]">{step.step}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">Tiempo: {step.timeSpent}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className={`text-sm font-medium ${getCompletionColor(step.quality)}`}>
|
||||
{step.quality}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">Calidad</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Comparación con la Industria</h3>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Puntuación de Configuración</span>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-bold text-[var(--color-success)]">{benchmarkComparison.yourData.onboardingScore}</span>
|
||||
<span className="text-sm text-[var(--text-tertiary)] ml-2">vs {benchmarkComparison.industry.onboardingScore}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
|
||||
<div className="bg-green-600 h-2 rounded-full" style={{ width: `${(benchmarkComparison.yourData.onboardingScore / 100) * 100}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Tasa de Completado</span>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-bold text-[var(--color-info)]">{benchmarkComparison.yourData.completionRate}%</span>
|
||||
<span className="text-sm text-[var(--text-tertiary)] ml-2">vs {benchmarkComparison.industry.completionRate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
|
||||
<div className="bg-blue-600 h-2 rounded-full" style={{ width: `${benchmarkComparison.yourData.completionRate}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Tiempo de Configuración</span>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-bold text-purple-600">{benchmarkComparison.yourData.averageTime}</span>
|
||||
<span className="text-sm text-[var(--text-tertiary)] ml-2">vs {benchmarkComparison.industry.averageTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-green-50 p-3 rounded-lg">
|
||||
<p className="text-sm text-[var(--color-success)]">38% más rápido que el promedio de la industria</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Insights */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Insights y Recomendaciones</h3>
|
||||
<div className="space-y-4">
|
||||
{insights.map((insight, index) => (
|
||||
<div key={index} className={`p-4 rounded-lg border ${getInsightColor(insight.type)}`}>
|
||||
<div className="flex items-start space-x-3">
|
||||
{getInsightIcon(insight.type)}
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-1">{insight.title}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-2">{insight.description}</p>
|
||||
<p className="text-sm font-medium text-[var(--text-primary)]">
|
||||
Recomendación: {insight.recommendation}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={insight.impact === 'high' ? 'red' : insight.impact === 'medium' ? 'yellow' : 'green'}>
|
||||
{insight.impact === 'high' ? 'Alto' : insight.impact === 'medium' ? 'Medio' : 'Bajo'} Impacto
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Data Analysis */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Análisis de Calidad de Datos</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{dataAnalysis.map((category, index) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="font-medium text-[var(--text-primary)]">{category.category}</h4>
|
||||
<div className="flex items-center space-x-2">
|
||||
<BarChart3 className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<span className="text-sm text-[var(--text-tertiary)]">{category.items} elementos</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>Completitud</span>
|
||||
<span className={getCompletionColor(category.completeness)}>{category.completeness}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${category.completeness}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>Precisión</span>
|
||||
<span className={getCompletionColor(category.accuracy)}>{category.accuracy}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${category.accuracy}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{category.issues > 0 && (
|
||||
<div className="flex items-center text-sm text-[var(--color-error)]">
|
||||
<AlertCircle className="w-4 h-4 mr-1" />
|
||||
<span>{category.issues} problema{category.issues > 1 ? 's' : ''} detectado{category.issues > 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-[var(--text-secondary)]">{category.details}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Action Items */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Elementos de Acción</h3>
|
||||
<div className="space-y-3">
|
||||
{recommendations.map((rec, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-start space-x-3 flex-1">
|
||||
<Badge variant={getPriorityColor(rec.priority)}>
|
||||
{rec.priority === 'high' ? 'Alta' : rec.priority === 'medium' ? 'Media' : 'Baja'}
|
||||
</Badge>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-1">{rec.title}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-1">{rec.description}</p>
|
||||
<div className="flex items-center space-x-4 text-xs text-[var(--text-tertiary)]">
|
||||
<span>Tiempo estimado: {rec.estimatedTime}</span>
|
||||
<span>•</span>
|
||||
<span>Impacto: {rec.impact}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm">
|
||||
Completar
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.location.href = '/app/onboarding/upload'}
|
||||
>
|
||||
← Volver a Carga de Datos
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => window.location.href = '/app/onboarding/review'}
|
||||
>
|
||||
Continuar a Revisión Final →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingAnalysisPage;
|
||||
@@ -1,435 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { BarChart3, TrendingUp, Target, AlertCircle, CheckCircle, Eye, Download } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const OnboardingAnalysisPage: React.FC = () => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('30days');
|
||||
|
||||
const analysisData = {
|
||||
onboardingScore: 87,
|
||||
completionRate: 92,
|
||||
averageTime: '4.2 días',
|
||||
stepsCompleted: 15,
|
||||
totalSteps: 16,
|
||||
dataQuality: 94
|
||||
};
|
||||
|
||||
const stepProgress = [
|
||||
{ step: 'Información Básica', completed: true, quality: 95, timeSpent: '25 min' },
|
||||
{ step: 'Configuración de Menú', completed: true, quality: 88, timeSpent: '1.2 horas' },
|
||||
{ step: 'Datos de Inventario', completed: true, quality: 92, timeSpent: '45 min' },
|
||||
{ step: 'Configuración de Horarios', completed: true, quality: 100, timeSpent: '15 min' },
|
||||
{ step: 'Integración de Pagos', completed: true, quality: 85, timeSpent: '30 min' },
|
||||
{ step: 'Carga de Productos', completed: true, quality: 90, timeSpent: '2.1 horas' },
|
||||
{ step: 'Configuración de Personal', completed: true, quality: 87, timeSpent: '40 min' },
|
||||
{ step: 'Pruebas del Sistema', completed: false, quality: 0, timeSpent: '-' }
|
||||
];
|
||||
|
||||
const insights = [
|
||||
{
|
||||
type: 'success',
|
||||
title: 'Excelente Progreso',
|
||||
description: 'Has completado el 94% del proceso de configuración inicial',
|
||||
recommendation: 'Solo faltan las pruebas finales del sistema para completar tu configuración',
|
||||
impact: 'high'
|
||||
},
|
||||
{
|
||||
type: 'info',
|
||||
title: 'Calidad de Datos Alta',
|
||||
description: 'Tus datos tienen una calidad promedio del 94%',
|
||||
recommendation: 'Considera revisar la configuración de pagos para mejorar la puntuación',
|
||||
impact: 'medium'
|
||||
},
|
||||
{
|
||||
type: 'warning',
|
||||
title: 'Paso Pendiente',
|
||||
description: 'Las pruebas del sistema están pendientes',
|
||||
recommendation: 'Programa las pruebas para validar la configuración completa',
|
||||
impact: 'high'
|
||||
}
|
||||
];
|
||||
|
||||
const dataAnalysis = [
|
||||
{
|
||||
category: 'Información del Negocio',
|
||||
completeness: 100,
|
||||
accuracy: 95,
|
||||
items: 12,
|
||||
issues: 0,
|
||||
details: 'Toda la información básica está completa y verificada'
|
||||
},
|
||||
{
|
||||
category: 'Menú y Productos',
|
||||
completeness: 85,
|
||||
accuracy: 88,
|
||||
items: 45,
|
||||
issues: 3,
|
||||
details: '3 productos sin precios definidos'
|
||||
},
|
||||
{
|
||||
category: 'Inventario Inicial',
|
||||
completeness: 92,
|
||||
accuracy: 90,
|
||||
items: 28,
|
||||
issues: 2,
|
||||
details: '2 ingredientes sin stock mínimo definido'
|
||||
},
|
||||
{
|
||||
category: 'Configuración Operativa',
|
||||
completeness: 100,
|
||||
accuracy: 100,
|
||||
items: 8,
|
||||
issues: 0,
|
||||
details: 'Horarios y políticas completamente configuradas'
|
||||
}
|
||||
];
|
||||
|
||||
const benchmarkComparison = {
|
||||
industry: {
|
||||
onboardingScore: 74,
|
||||
completionRate: 78,
|
||||
averageTime: '6.8 días'
|
||||
},
|
||||
yourData: {
|
||||
onboardingScore: 87,
|
||||
completionRate: 92,
|
||||
averageTime: '4.2 días'
|
||||
}
|
||||
};
|
||||
|
||||
const recommendations = [
|
||||
{
|
||||
priority: 'high',
|
||||
title: 'Completar Pruebas del Sistema',
|
||||
description: 'Realizar pruebas integrales para validar toda la configuración',
|
||||
estimatedTime: '30 minutos',
|
||||
impact: 'Garantiza funcionamiento óptimo del sistema'
|
||||
},
|
||||
{
|
||||
priority: 'medium',
|
||||
title: 'Revisar Precios de Productos',
|
||||
description: 'Definir precios para los 3 productos pendientes',
|
||||
estimatedTime: '15 minutos',
|
||||
impact: 'Permitirá generar ventas de todos los productos'
|
||||
},
|
||||
{
|
||||
priority: 'medium',
|
||||
title: 'Configurar Stocks Mínimos',
|
||||
description: 'Establecer niveles mínimos para 2 ingredientes',
|
||||
estimatedTime: '10 minutos',
|
||||
impact: 'Mejorará el control de inventario automático'
|
||||
},
|
||||
{
|
||||
priority: 'low',
|
||||
title: 'Optimizar Configuración de Pagos',
|
||||
description: 'Revisar métodos de pago y comisiones',
|
||||
estimatedTime: '20 minutos',
|
||||
impact: 'Puede reducir costos de transacción'
|
||||
}
|
||||
];
|
||||
|
||||
const getInsightIcon = (type: string) => {
|
||||
const iconProps = { className: "w-5 h-5" };
|
||||
switch (type) {
|
||||
case 'success': return <CheckCircle {...iconProps} className="w-5 h-5 text-green-600" />;
|
||||
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
|
||||
case 'info': return <Target {...iconProps} className="w-5 h-5 text-blue-600" />;
|
||||
default: return <AlertCircle {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getInsightColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'success': return 'bg-green-50 border-green-200';
|
||||
case 'warning': return 'bg-yellow-50 border-yellow-200';
|
||||
case 'info': return 'bg-blue-50 border-blue-200';
|
||||
default: return 'bg-gray-50 border-gray-200';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'red';
|
||||
case 'medium': return 'yellow';
|
||||
case 'low': return 'green';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getCompletionColor = (percentage: number) => {
|
||||
if (percentage >= 95) return 'text-green-600';
|
||||
if (percentage >= 80) return 'text-yellow-600';
|
||||
return 'text-red-600';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Análisis de Configuración"
|
||||
description="Análisis detallado de tu proceso de configuración y recomendaciones"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Ver Detalles
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar Reporte
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Overall Score */}
|
||||
<Card className="p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-6">
|
||||
<div className="text-center">
|
||||
<div className="relative w-24 h-24 mx-auto mb-3">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-2xl font-bold text-green-600">{analysisData.onboardingScore}</span>
|
||||
</div>
|
||||
<svg className="w-24 h-24 transform -rotate-90">
|
||||
<circle
|
||||
cx="48"
|
||||
cy="48"
|
||||
r="40"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
fill="none"
|
||||
className="text-gray-200"
|
||||
/>
|
||||
<circle
|
||||
cx="48"
|
||||
cy="48"
|
||||
r="40"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
fill="none"
|
||||
strokeDasharray={`${analysisData.onboardingScore * 2.51} 251`}
|
||||
className="text-green-600"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-gray-700">Puntuación General</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-blue-600">{analysisData.completionRate}%</p>
|
||||
<p className="text-sm font-medium text-gray-700">Completado</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-purple-600">{analysisData.averageTime}</p>
|
||||
<p className="text-sm font-medium text-gray-700">Tiempo Promedio</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-orange-600">{analysisData.stepsCompleted}/{analysisData.totalSteps}</p>
|
||||
<p className="text-sm font-medium text-gray-700">Pasos Completados</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-teal-600">{analysisData.dataQuality}%</p>
|
||||
<p className="text-sm font-medium text-gray-700">Calidad de Datos</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center mb-2">
|
||||
<TrendingUp className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-gray-700">Por encima del promedio</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Progress Analysis */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Progreso por Pasos</h3>
|
||||
<div className="space-y-4">
|
||||
{stepProgress.map((step, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
step.completed ? 'bg-green-100' : 'bg-gray-100'
|
||||
}`}>
|
||||
{step.completed ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
) : (
|
||||
<span className="text-sm font-medium text-gray-500">{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">{step.step}</p>
|
||||
<p className="text-xs text-gray-500">Tiempo: {step.timeSpent}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className={`text-sm font-medium ${getCompletionColor(step.quality)}`}>
|
||||
{step.quality}%
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">Calidad</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Comparación con la Industria</h3>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-gray-700">Puntuación de Configuración</span>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-bold text-green-600">{benchmarkComparison.yourData.onboardingScore}</span>
|
||||
<span className="text-sm text-gray-500 ml-2">vs {benchmarkComparison.industry.onboardingScore}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div className="bg-green-600 h-2 rounded-full" style={{ width: `${(benchmarkComparison.yourData.onboardingScore / 100) * 100}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-gray-700">Tasa de Completado</span>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-bold text-blue-600">{benchmarkComparison.yourData.completionRate}%</span>
|
||||
<span className="text-sm text-gray-500 ml-2">vs {benchmarkComparison.industry.completionRate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div className="bg-blue-600 h-2 rounded-full" style={{ width: `${benchmarkComparison.yourData.completionRate}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-gray-700">Tiempo de Configuración</span>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-bold text-purple-600">{benchmarkComparison.yourData.averageTime}</span>
|
||||
<span className="text-sm text-gray-500 ml-2">vs {benchmarkComparison.industry.averageTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-green-50 p-3 rounded-lg">
|
||||
<p className="text-sm text-green-700">38% más rápido que el promedio de la industria</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Insights */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Insights y Recomendaciones</h3>
|
||||
<div className="space-y-4">
|
||||
{insights.map((insight, index) => (
|
||||
<div key={index} className={`p-4 rounded-lg border ${getInsightColor(insight.type)}`}>
|
||||
<div className="flex items-start space-x-3">
|
||||
{getInsightIcon(insight.type)}
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-1">{insight.title}</h4>
|
||||
<p className="text-sm text-gray-700 mb-2">{insight.description}</p>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
Recomendación: {insight.recommendation}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={insight.impact === 'high' ? 'red' : insight.impact === 'medium' ? 'yellow' : 'green'}>
|
||||
{insight.impact === 'high' ? 'Alto' : insight.impact === 'medium' ? 'Medio' : 'Bajo'} Impacto
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Data Analysis */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Análisis de Calidad de Datos</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{dataAnalysis.map((category, index) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="font-medium text-gray-900">{category.category}</h4>
|
||||
<div className="flex items-center space-x-2">
|
||||
<BarChart3 className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-sm text-gray-500">{category.items} elementos</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>Completitud</span>
|
||||
<span className={getCompletionColor(category.completeness)}>{category.completeness}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${category.completeness}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>Precisión</span>
|
||||
<span className={getCompletionColor(category.accuracy)}>{category.accuracy}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${category.accuracy}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{category.issues > 0 && (
|
||||
<div className="flex items-center text-sm text-red-600">
|
||||
<AlertCircle className="w-4 h-4 mr-1" />
|
||||
<span>{category.issues} problema{category.issues > 1 ? 's' : ''} detectado{category.issues > 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-gray-600">{category.details}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Action Items */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Elementos de Acción</h3>
|
||||
<div className="space-y-3">
|
||||
{recommendations.map((rec, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-start space-x-3 flex-1">
|
||||
<Badge variant={getPriorityColor(rec.priority)}>
|
||||
{rec.priority === 'high' ? 'Alta' : rec.priority === 'medium' ? 'Media' : 'Baja'}
|
||||
</Badge>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-1">{rec.title}</h4>
|
||||
<p className="text-sm text-gray-600 mb-1">{rec.description}</p>
|
||||
<div className="flex items-center space-x-4 text-xs text-gray-500">
|
||||
<span>Tiempo estimado: {rec.estimatedTime}</span>
|
||||
<span>•</span>
|
||||
<span>Impacto: {rec.impact}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm">
|
||||
Completar
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingAnalysisPage;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as OnboardingAnalysisPage } from './OnboardingAnalysisPage';
|
||||
@@ -1,610 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CheckCircle, AlertCircle, Edit2, Eye, Star, ArrowRight, Clock, Users, Zap } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const OnboardingReviewPage: React.FC = () => {
|
||||
const [activeSection, setActiveSection] = useState<string>('overview');
|
||||
|
||||
const completionData = {
|
||||
overallProgress: 95,
|
||||
totalSteps: 8,
|
||||
completedSteps: 7,
|
||||
remainingSteps: 1,
|
||||
estimatedTimeRemaining: '15 minutos',
|
||||
overallScore: 87
|
||||
};
|
||||
|
||||
const sectionReview = [
|
||||
{
|
||||
id: 'business-info',
|
||||
title: 'Información del Negocio',
|
||||
status: 'completed',
|
||||
score: 98,
|
||||
items: [
|
||||
{ field: 'Nombre de la panadería', value: 'Panadería Artesanal El Buen Pan', status: 'complete' },
|
||||
{ field: 'Dirección', value: 'Av. Principal 123, Centro', status: 'complete' },
|
||||
{ field: 'Teléfono', value: '+1 234 567 8900', status: 'complete' },
|
||||
{ field: 'Email', value: 'info@elbuenpan.com', status: 'complete' },
|
||||
{ field: 'Tipo de negocio', value: 'Panadería y Pastelería Artesanal', status: 'complete' },
|
||||
{ field: 'Horario de operación', value: 'L-V: 6:00-20:00, S-D: 7:00-18:00', status: 'complete' }
|
||||
],
|
||||
recommendations: []
|
||||
},
|
||||
{
|
||||
id: 'menu-products',
|
||||
title: 'Menú y Productos',
|
||||
status: 'completed',
|
||||
score: 85,
|
||||
items: [
|
||||
{ field: 'Productos de panadería', value: '24 productos configurados', status: 'complete' },
|
||||
{ field: 'Productos de pastelería', value: '18 productos configurados', status: 'complete' },
|
||||
{ field: 'Bebidas', value: '12 opciones disponibles', status: 'complete' },
|
||||
{ field: 'Precios configurados', value: '51 de 54 productos (94%)', status: 'warning' },
|
||||
{ field: 'Categorías organizadas', value: '6 categorías principales', status: 'complete' },
|
||||
{ field: 'Descripciones', value: '48 de 54 productos (89%)', status: 'warning' }
|
||||
],
|
||||
recommendations: [
|
||||
'Completar precios para 3 productos pendientes',
|
||||
'Añadir descripciones para 6 productos restantes'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'inventory',
|
||||
title: 'Inventario Inicial',
|
||||
status: 'completed',
|
||||
score: 92,
|
||||
items: [
|
||||
{ field: 'Materias primas', value: '45 ingredientes registrados', status: 'complete' },
|
||||
{ field: 'Proveedores', value: '8 proveedores configurados', status: 'complete' },
|
||||
{ field: 'Stocks iniciales', value: '43 de 45 ingredientes (96%)', status: 'warning' },
|
||||
{ field: 'Puntos de reorden', value: '40 de 45 ingredientes (89%)', status: 'warning' },
|
||||
{ field: 'Costos unitarios', value: 'Completado al 100%', status: 'complete' }
|
||||
],
|
||||
recommendations: [
|
||||
'Definir stocks iniciales para 2 ingredientes',
|
||||
'Establecer puntos de reorden para 5 ingredientes'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'staff-config',
|
||||
title: 'Configuración de Personal',
|
||||
status: 'completed',
|
||||
score: 90,
|
||||
items: [
|
||||
{ field: 'Empleados registrados', value: '12 miembros del equipo', status: 'complete' },
|
||||
{ field: 'Roles asignados', value: '5 roles diferentes configurados', status: 'complete' },
|
||||
{ field: 'Horarios de trabajo', value: '11 de 12 empleados (92%)', status: 'warning' },
|
||||
{ field: 'Permisos del sistema', value: 'Configurado completamente', status: 'complete' },
|
||||
{ field: 'Datos de contacto', value: 'Completado al 100%', status: 'complete' }
|
||||
],
|
||||
recommendations: [
|
||||
'Completar horario para 1 empleado pendiente'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'operations',
|
||||
title: 'Configuración Operativa',
|
||||
status: 'completed',
|
||||
score: 95,
|
||||
items: [
|
||||
{ field: 'Horarios de operación', value: 'Configurado completamente', status: 'complete' },
|
||||
{ field: 'Métodos de pago', value: '4 métodos activos', status: 'complete' },
|
||||
{ field: 'Políticas de devoluciones', value: 'Definidas y configuradas', status: 'complete' },
|
||||
{ field: 'Configuración de impuestos', value: '18% IVA configurado', status: 'complete' },
|
||||
{ field: 'Zonas de entrega', value: '3 zonas configuradas', status: 'complete' }
|
||||
],
|
||||
recommendations: []
|
||||
},
|
||||
{
|
||||
id: 'integrations',
|
||||
title: 'Integraciones',
|
||||
status: 'completed',
|
||||
score: 88,
|
||||
items: [
|
||||
{ field: 'Sistema de pagos', value: 'Stripe configurado', status: 'complete' },
|
||||
{ field: 'Notificaciones por email', value: 'SMTP configurado', status: 'complete' },
|
||||
{ field: 'Notificaciones SMS', value: 'Twilio configurado', status: 'complete' },
|
||||
{ field: 'Backup automático', value: 'Configurado diariamente', status: 'complete' },
|
||||
{ field: 'APIs externas', value: '2 de 3 configuradas', status: 'warning' }
|
||||
],
|
||||
recommendations: [
|
||||
'Configurar API de delivery restante'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'testing',
|
||||
title: 'Pruebas del Sistema',
|
||||
status: 'pending',
|
||||
score: 0,
|
||||
items: [
|
||||
{ field: 'Prueba de ventas', value: 'Pendiente', status: 'pending' },
|
||||
{ field: 'Prueba de inventario', value: 'Pendiente', status: 'pending' },
|
||||
{ field: 'Prueba de reportes', value: 'Pendiente', status: 'pending' },
|
||||
{ field: 'Prueba de notificaciones', value: 'Pendiente', status: 'pending' },
|
||||
{ field: 'Validación de integraciones', value: 'Pendiente', status: 'pending' }
|
||||
],
|
||||
recommendations: [
|
||||
'Ejecutar todas las pruebas del sistema antes del lanzamiento'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'training',
|
||||
title: 'Capacitación del Equipo',
|
||||
status: 'completed',
|
||||
score: 82,
|
||||
items: [
|
||||
{ field: 'Capacitación básica', value: '10 de 12 empleados (83%)', status: 'warning' },
|
||||
{ field: 'Manual del usuario', value: 'Entregado y revisado', status: 'complete' },
|
||||
{ field: 'Sesiones prácticas', value: '2 de 3 sesiones completadas', status: 'warning' },
|
||||
{ field: 'Evaluaciones', value: '8 de 10 empleados aprobados', status: 'warning' },
|
||||
{ field: 'Material de referencia', value: 'Disponible en el sistema', status: 'complete' }
|
||||
],
|
||||
recommendations: [
|
||||
'Completar capacitación para 2 empleados pendientes',
|
||||
'Programar tercera sesión práctica',
|
||||
'Realizar evaluaciones pendientes'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const overallRecommendations = [
|
||||
{
|
||||
priority: 'high',
|
||||
category: 'Crítico',
|
||||
title: 'Completar Pruebas del Sistema',
|
||||
description: 'Ejecutar todas las pruebas funcionales antes del lanzamiento',
|
||||
estimatedTime: '30 minutos',
|
||||
impact: 'Garantiza funcionamiento correcto del sistema'
|
||||
},
|
||||
{
|
||||
priority: 'medium',
|
||||
category: 'Importante',
|
||||
title: 'Finalizar Configuración de Productos',
|
||||
description: 'Completar precios y descripciones pendientes',
|
||||
estimatedTime: '20 minutos',
|
||||
impact: 'Permite ventas completas de todos los productos'
|
||||
},
|
||||
{
|
||||
priority: 'medium',
|
||||
category: 'Importante',
|
||||
title: 'Completar Capacitación del Personal',
|
||||
description: 'Finalizar entrenamiento para empleados pendientes',
|
||||
estimatedTime: '45 minutos',
|
||||
impact: 'Asegura operación eficiente desde el primer día'
|
||||
},
|
||||
{
|
||||
priority: 'low',
|
||||
category: 'Opcional',
|
||||
title: 'Optimizar Configuración de Inventario',
|
||||
description: 'Definir stocks y puntos de reorden pendientes',
|
||||
estimatedTime: '15 minutos',
|
||||
impact: 'Mejora control automático de inventario'
|
||||
}
|
||||
];
|
||||
|
||||
const launchReadiness = {
|
||||
essential: {
|
||||
completed: 6,
|
||||
total: 7,
|
||||
percentage: 86
|
||||
},
|
||||
recommended: {
|
||||
completed: 8,
|
||||
total: 12,
|
||||
percentage: 67
|
||||
},
|
||||
optional: {
|
||||
completed: 3,
|
||||
total: 6,
|
||||
percentage: 50
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
const iconProps = { className: "w-5 h-5" };
|
||||
switch (status) {
|
||||
case 'completed': return <CheckCircle {...iconProps} className="w-5 h-5 text-[var(--color-success)]" />;
|
||||
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
|
||||
case 'pending': return <Clock {...iconProps} className="w-5 h-5 text-[var(--text-secondary)]" />;
|
||||
default: return <AlertCircle {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'green';
|
||||
case 'warning': return 'yellow';
|
||||
case 'pending': return 'gray';
|
||||
default: return 'red';
|
||||
}
|
||||
};
|
||||
|
||||
const getItemStatusIcon = (status: string) => {
|
||||
const iconProps = { className: "w-4 h-4" };
|
||||
switch (status) {
|
||||
case 'complete': return <CheckCircle {...iconProps} className="w-4 h-4 text-[var(--color-success)]" />;
|
||||
case 'warning': return <AlertCircle {...iconProps} className="w-4 h-4 text-yellow-600" />;
|
||||
case 'pending': return <Clock {...iconProps} className="w-4 h-4 text-[var(--text-secondary)]" />;
|
||||
default: return <AlertCircle {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'red';
|
||||
case 'medium': return 'yellow';
|
||||
case 'low': return 'green';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 90) return 'text-[var(--color-success)]';
|
||||
if (score >= 80) return 'text-yellow-600';
|
||||
if (score >= 70) return 'text-[var(--color-primary)]';
|
||||
return 'text-[var(--color-error)]';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Revisión Final de Configuración"
|
||||
description="Verifica todos los aspectos de tu configuración antes del lanzamiento"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline">
|
||||
<Edit2 className="w-4 h-4 mr-2" />
|
||||
Editar Configuración
|
||||
</Button>
|
||||
<Button>
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
Lanzar Sistema
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Overall Progress */}
|
||||
<Card className="p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div className="text-center">
|
||||
<div className="relative w-20 h-20 mx-auto mb-3">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className={`text-2xl font-bold ${getScoreColor(completionData.overallScore)}`}>
|
||||
{completionData.overallScore}
|
||||
</span>
|
||||
</div>
|
||||
<svg className="w-20 h-20 transform -rotate-90">
|
||||
<circle
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="32"
|
||||
stroke="currentColor"
|
||||
strokeWidth="6"
|
||||
fill="none"
|
||||
className="text-gray-200"
|
||||
/>
|
||||
<circle
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="32"
|
||||
stroke="currentColor"
|
||||
strokeWidth="6"
|
||||
fill="none"
|
||||
strokeDasharray={`${completionData.overallScore * 2.01} 201`}
|
||||
className="text-[var(--color-success)]"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Puntuación General</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-[var(--color-info)]">{completionData.overallProgress}%</p>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Progreso Total</p>
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{ width: `${completionData.overallProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-purple-600">
|
||||
{completionData.completedSteps}/{completionData.totalSteps}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Secciones Completadas</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-[var(--color-primary)]">{completionData.estimatedTimeRemaining}</p>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Tiempo Restante</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="border-b border-[var(--border-primary)]">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
{['overview', 'sections', 'recommendations', 'readiness'].map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveSection(tab)}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeSection === tab
|
||||
? 'border-blue-500 text-[var(--color-info)]'
|
||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
{tab === 'overview' && 'Resumen General'}
|
||||
{tab === 'sections' && 'Revisión por Secciones'}
|
||||
{tab === 'recommendations' && 'Recomendaciones'}
|
||||
{tab === 'readiness' && 'Preparación para Lanzamiento'}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Content based on active section */}
|
||||
{activeSection === 'overview' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Estado por Secciones</h3>
|
||||
<div className="space-y-3">
|
||||
{sectionReview.map((section) => (
|
||||
<div key={section.id} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStatusIcon(section.status)}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-primary)]">{section.title}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">
|
||||
{section.recommendations.length > 0
|
||||
? `${section.recommendations.length} recomendación${section.recommendations.length > 1 ? 'es' : ''}`
|
||||
: 'Completado correctamente'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className={`text-sm font-medium ${getScoreColor(section.score)}`}>
|
||||
{section.score}%
|
||||
</p>
|
||||
<Badge variant={getStatusColor(section.status)}>
|
||||
{section.status === 'completed' ? 'Completado' :
|
||||
section.status === 'warning' ? 'Con observaciones' : 'Pendiente'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Próximos Pasos</h3>
|
||||
<div className="space-y-4">
|
||||
{overallRecommendations.slice(0, 3).map((rec, index) => (
|
||||
<div key={index} className="flex items-start space-x-3 p-3 border rounded-lg">
|
||||
<Badge variant={getPriorityColor(rec.priority)} className="mt-1">
|
||||
{rec.category}
|
||||
</Badge>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)]">{rec.title}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-1">{rec.description}</p>
|
||||
<div className="flex items-center space-x-4 mt-2 text-xs text-[var(--text-tertiary)]">
|
||||
<span>⏱️ {rec.estimatedTime}</span>
|
||||
<span>💡 {rec.impact}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="w-4 h-4 text-[var(--text-tertiary)] mt-1" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-[var(--color-info)]/5 rounded-lg">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<Star className="w-5 h-5 text-[var(--color-info)]" />
|
||||
<h4 className="font-medium text-blue-900">¡Excelente progreso!</h4>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--color-info)]">
|
||||
Has completado el 95% de la configuración. Solo quedan algunos detalles finales antes del lanzamiento.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'sections' && (
|
||||
<div className="space-y-6">
|
||||
{sectionReview.map((section) => (
|
||||
<Card key={section.id} className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStatusIcon(section.status)}
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{section.title}</h3>
|
||||
<Badge variant={getStatusColor(section.status)}>
|
||||
Puntuación: {section.score}%
|
||||
</Badge>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Ver Detalles
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
{section.items.map((item, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
{getItemStatusIcon(item.status)}
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">{item.field}</span>
|
||||
</div>
|
||||
<span className="text-sm text-[var(--text-secondary)]">{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{section.recommendations.length > 0 && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<h4 className="text-sm font-medium text-yellow-800 mb-2">Recomendaciones:</h4>
|
||||
<ul className="text-sm text-yellow-700 space-y-1">
|
||||
{section.recommendations.map((rec, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<span className="mr-2">•</span>
|
||||
<span>{rec}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'recommendations' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Todas las Recomendaciones</h3>
|
||||
<div className="space-y-4">
|
||||
{overallRecommendations.map((rec, index) => (
|
||||
<div key={index} className="flex items-start justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-start space-x-3 flex-1">
|
||||
<Badge variant={getPriorityColor(rec.priority)}>
|
||||
{rec.category}
|
||||
</Badge>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-1">{rec.title}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-2">{rec.description}</p>
|
||||
<div className="flex items-center space-x-4 text-xs text-[var(--text-tertiary)]">
|
||||
<span>⏱️ Tiempo estimado: {rec.estimatedTime}</span>
|
||||
<span>💡 Impacto: {rec.impact}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" variant="outline">Más Información</Button>
|
||||
<Button size="sm">Completar</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeSection === 'readiness' && (
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Preparación para el Lanzamiento</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<div className="w-16 h-16 mx-auto mb-3 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-8 h-8 text-[var(--color-success)]" />
|
||||
</div>
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Elementos Esenciales</h4>
|
||||
<p className="text-2xl font-bold text-[var(--color-success)] mb-1">
|
||||
{launchReadiness.essential.completed}/{launchReadiness.essential.total}
|
||||
</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{launchReadiness.essential.percentage}% completado</p>
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-green-600 h-2 rounded-full"
|
||||
style={{ width: `${launchReadiness.essential.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<div className="w-16 h-16 mx-auto mb-3 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||
<Star className="w-8 h-8 text-yellow-600" />
|
||||
</div>
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Recomendados</h4>
|
||||
<p className="text-2xl font-bold text-yellow-600 mb-1">
|
||||
{launchReadiness.recommended.completed}/{launchReadiness.recommended.total}
|
||||
</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{launchReadiness.recommended.percentage}% completado</p>
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-yellow-600 h-2 rounded-full"
|
||||
style={{ width: `${launchReadiness.recommended.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<div className="w-16 h-16 mx-auto mb-3 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
||||
<Users className="w-8 h-8 text-[var(--color-info)]" />
|
||||
</div>
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Opcionales</h4>
|
||||
<p className="text-2xl font-bold text-[var(--color-info)] mb-1">
|
||||
{launchReadiness.optional.completed}/{launchReadiness.optional.total}
|
||||
</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{launchReadiness.optional.percentage}% completado</p>
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{ width: `${launchReadiness.optional.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-6">
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<CheckCircle className="w-6 h-6 text-[var(--color-success)]" />
|
||||
<h4 className="text-lg font-semibold text-green-900">¡Listo para Lanzar!</h4>
|
||||
</div>
|
||||
<p className="text-[var(--color-success)] mb-4">
|
||||
Tu configuración está lista para el lanzamiento. Todos los elementos esenciales están completados
|
||||
y el sistema está preparado para comenzar a operar.
|
||||
</p>
|
||||
<div className="flex space-x-3">
|
||||
<Button
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
onClick={() => {
|
||||
alert('¡Felicitaciones! El onboarding ha sido completado exitosamente. Ahora puedes comenzar a usar la plataforma.');
|
||||
window.location.href = '/app/dashboard';
|
||||
}}
|
||||
>
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
Lanzar Ahora
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
Ejecutar Pruebas Finales
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overall Navigation - Always visible */}
|
||||
<div className="flex justify-between items-center mt-8 p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.location.href = '/app/onboarding/analysis'}
|
||||
>
|
||||
← Volver al Análisis
|
||||
</Button>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-[var(--text-secondary)]">Último paso del onboarding</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
onClick={() => {
|
||||
alert('¡Felicitaciones! El onboarding ha sido completado exitosamente. Ahora puedes comenzar a usar la plataforma.');
|
||||
window.location.href = '/app/dashboard';
|
||||
}}
|
||||
>
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
Finalizar Onboarding
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingReviewPage;
|
||||
@@ -1,579 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CheckCircle, AlertCircle, Edit2, Eye, Star, ArrowRight, Clock, Users, Zap } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const OnboardingReviewPage: React.FC = () => {
|
||||
const [activeSection, setActiveSection] = useState<string>('overview');
|
||||
|
||||
const completionData = {
|
||||
overallProgress: 95,
|
||||
totalSteps: 8,
|
||||
completedSteps: 7,
|
||||
remainingSteps: 1,
|
||||
estimatedTimeRemaining: '15 minutos',
|
||||
overallScore: 87
|
||||
};
|
||||
|
||||
const sectionReview = [
|
||||
{
|
||||
id: 'business-info',
|
||||
title: 'Información del Negocio',
|
||||
status: 'completed',
|
||||
score: 98,
|
||||
items: [
|
||||
{ field: 'Nombre de la panadería', value: 'Panadería Artesanal El Buen Pan', status: 'complete' },
|
||||
{ field: 'Dirección', value: 'Av. Principal 123, Centro', status: 'complete' },
|
||||
{ field: 'Teléfono', value: '+1 234 567 8900', status: 'complete' },
|
||||
{ field: 'Email', value: 'info@elbuenpan.com', status: 'complete' },
|
||||
{ field: 'Tipo de negocio', value: 'Panadería y Pastelería Artesanal', status: 'complete' },
|
||||
{ field: 'Horario de operación', value: 'L-V: 6:00-20:00, S-D: 7:00-18:00', status: 'complete' }
|
||||
],
|
||||
recommendations: []
|
||||
},
|
||||
{
|
||||
id: 'menu-products',
|
||||
title: 'Menú y Productos',
|
||||
status: 'completed',
|
||||
score: 85,
|
||||
items: [
|
||||
{ field: 'Productos de panadería', value: '24 productos configurados', status: 'complete' },
|
||||
{ field: 'Productos de pastelería', value: '18 productos configurados', status: 'complete' },
|
||||
{ field: 'Bebidas', value: '12 opciones disponibles', status: 'complete' },
|
||||
{ field: 'Precios configurados', value: '51 de 54 productos (94%)', status: 'warning' },
|
||||
{ field: 'Categorías organizadas', value: '6 categorías principales', status: 'complete' },
|
||||
{ field: 'Descripciones', value: '48 de 54 productos (89%)', status: 'warning' }
|
||||
],
|
||||
recommendations: [
|
||||
'Completar precios para 3 productos pendientes',
|
||||
'Añadir descripciones para 6 productos restantes'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'inventory',
|
||||
title: 'Inventario Inicial',
|
||||
status: 'completed',
|
||||
score: 92,
|
||||
items: [
|
||||
{ field: 'Materias primas', value: '45 ingredientes registrados', status: 'complete' },
|
||||
{ field: 'Proveedores', value: '8 proveedores configurados', status: 'complete' },
|
||||
{ field: 'Stocks iniciales', value: '43 de 45 ingredientes (96%)', status: 'warning' },
|
||||
{ field: 'Puntos de reorden', value: '40 de 45 ingredientes (89%)', status: 'warning' },
|
||||
{ field: 'Costos unitarios', value: 'Completado al 100%', status: 'complete' }
|
||||
],
|
||||
recommendations: [
|
||||
'Definir stocks iniciales para 2 ingredientes',
|
||||
'Establecer puntos de reorden para 5 ingredientes'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'staff-config',
|
||||
title: 'Configuración de Personal',
|
||||
status: 'completed',
|
||||
score: 90,
|
||||
items: [
|
||||
{ field: 'Empleados registrados', value: '12 miembros del equipo', status: 'complete' },
|
||||
{ field: 'Roles asignados', value: '5 roles diferentes configurados', status: 'complete' },
|
||||
{ field: 'Horarios de trabajo', value: '11 de 12 empleados (92%)', status: 'warning' },
|
||||
{ field: 'Permisos del sistema', value: 'Configurado completamente', status: 'complete' },
|
||||
{ field: 'Datos de contacto', value: 'Completado al 100%', status: 'complete' }
|
||||
],
|
||||
recommendations: [
|
||||
'Completar horario para 1 empleado pendiente'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'operations',
|
||||
title: 'Configuración Operativa',
|
||||
status: 'completed',
|
||||
score: 95,
|
||||
items: [
|
||||
{ field: 'Horarios de operación', value: 'Configurado completamente', status: 'complete' },
|
||||
{ field: 'Métodos de pago', value: '4 métodos activos', status: 'complete' },
|
||||
{ field: 'Políticas de devoluciones', value: 'Definidas y configuradas', status: 'complete' },
|
||||
{ field: 'Configuración de impuestos', value: '18% IVA configurado', status: 'complete' },
|
||||
{ field: 'Zonas de entrega', value: '3 zonas configuradas', status: 'complete' }
|
||||
],
|
||||
recommendations: []
|
||||
},
|
||||
{
|
||||
id: 'integrations',
|
||||
title: 'Integraciones',
|
||||
status: 'completed',
|
||||
score: 88,
|
||||
items: [
|
||||
{ field: 'Sistema de pagos', value: 'Stripe configurado', status: 'complete' },
|
||||
{ field: 'Notificaciones por email', value: 'SMTP configurado', status: 'complete' },
|
||||
{ field: 'Notificaciones SMS', value: 'Twilio configurado', status: 'complete' },
|
||||
{ field: 'Backup automático', value: 'Configurado diariamente', status: 'complete' },
|
||||
{ field: 'APIs externas', value: '2 de 3 configuradas', status: 'warning' }
|
||||
],
|
||||
recommendations: [
|
||||
'Configurar API de delivery restante'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'testing',
|
||||
title: 'Pruebas del Sistema',
|
||||
status: 'pending',
|
||||
score: 0,
|
||||
items: [
|
||||
{ field: 'Prueba de ventas', value: 'Pendiente', status: 'pending' },
|
||||
{ field: 'Prueba de inventario', value: 'Pendiente', status: 'pending' },
|
||||
{ field: 'Prueba de reportes', value: 'Pendiente', status: 'pending' },
|
||||
{ field: 'Prueba de notificaciones', value: 'Pendiente', status: 'pending' },
|
||||
{ field: 'Validación de integraciones', value: 'Pendiente', status: 'pending' }
|
||||
],
|
||||
recommendations: [
|
||||
'Ejecutar todas las pruebas del sistema antes del lanzamiento'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'training',
|
||||
title: 'Capacitación del Equipo',
|
||||
status: 'completed',
|
||||
score: 82,
|
||||
items: [
|
||||
{ field: 'Capacitación básica', value: '10 de 12 empleados (83%)', status: 'warning' },
|
||||
{ field: 'Manual del usuario', value: 'Entregado y revisado', status: 'complete' },
|
||||
{ field: 'Sesiones prácticas', value: '2 de 3 sesiones completadas', status: 'warning' },
|
||||
{ field: 'Evaluaciones', value: '8 de 10 empleados aprobados', status: 'warning' },
|
||||
{ field: 'Material de referencia', value: 'Disponible en el sistema', status: 'complete' }
|
||||
],
|
||||
recommendations: [
|
||||
'Completar capacitación para 2 empleados pendientes',
|
||||
'Programar tercera sesión práctica',
|
||||
'Realizar evaluaciones pendientes'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const overallRecommendations = [
|
||||
{
|
||||
priority: 'high',
|
||||
category: 'Crítico',
|
||||
title: 'Completar Pruebas del Sistema',
|
||||
description: 'Ejecutar todas las pruebas funcionales antes del lanzamiento',
|
||||
estimatedTime: '30 minutos',
|
||||
impact: 'Garantiza funcionamiento correcto del sistema'
|
||||
},
|
||||
{
|
||||
priority: 'medium',
|
||||
category: 'Importante',
|
||||
title: 'Finalizar Configuración de Productos',
|
||||
description: 'Completar precios y descripciones pendientes',
|
||||
estimatedTime: '20 minutos',
|
||||
impact: 'Permite ventas completas de todos los productos'
|
||||
},
|
||||
{
|
||||
priority: 'medium',
|
||||
category: 'Importante',
|
||||
title: 'Completar Capacitación del Personal',
|
||||
description: 'Finalizar entrenamiento para empleados pendientes',
|
||||
estimatedTime: '45 minutos',
|
||||
impact: 'Asegura operación eficiente desde el primer día'
|
||||
},
|
||||
{
|
||||
priority: 'low',
|
||||
category: 'Opcional',
|
||||
title: 'Optimizar Configuración de Inventario',
|
||||
description: 'Definir stocks y puntos de reorden pendientes',
|
||||
estimatedTime: '15 minutos',
|
||||
impact: 'Mejora control automático de inventario'
|
||||
}
|
||||
];
|
||||
|
||||
const launchReadiness = {
|
||||
essential: {
|
||||
completed: 6,
|
||||
total: 7,
|
||||
percentage: 86
|
||||
},
|
||||
recommended: {
|
||||
completed: 8,
|
||||
total: 12,
|
||||
percentage: 67
|
||||
},
|
||||
optional: {
|
||||
completed: 3,
|
||||
total: 6,
|
||||
percentage: 50
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
const iconProps = { className: "w-5 h-5" };
|
||||
switch (status) {
|
||||
case 'completed': return <CheckCircle {...iconProps} className="w-5 h-5 text-green-600" />;
|
||||
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
|
||||
case 'pending': return <Clock {...iconProps} className="w-5 h-5 text-gray-600" />;
|
||||
default: return <AlertCircle {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'green';
|
||||
case 'warning': return 'yellow';
|
||||
case 'pending': return 'gray';
|
||||
default: return 'red';
|
||||
}
|
||||
};
|
||||
|
||||
const getItemStatusIcon = (status: string) => {
|
||||
const iconProps = { className: "w-4 h-4" };
|
||||
switch (status) {
|
||||
case 'complete': return <CheckCircle {...iconProps} className="w-4 h-4 text-green-600" />;
|
||||
case 'warning': return <AlertCircle {...iconProps} className="w-4 h-4 text-yellow-600" />;
|
||||
case 'pending': return <Clock {...iconProps} className="w-4 h-4 text-gray-600" />;
|
||||
default: return <AlertCircle {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'red';
|
||||
case 'medium': return 'yellow';
|
||||
case 'low': return 'green';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 90) return 'text-green-600';
|
||||
if (score >= 80) return 'text-yellow-600';
|
||||
if (score >= 70) return 'text-orange-600';
|
||||
return 'text-red-600';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Revisión Final de Configuración"
|
||||
description="Verifica todos los aspectos de tu configuración antes del lanzamiento"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline">
|
||||
<Edit2 className="w-4 h-4 mr-2" />
|
||||
Editar Configuración
|
||||
</Button>
|
||||
<Button>
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
Lanzar Sistema
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Overall Progress */}
|
||||
<Card className="p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div className="text-center">
|
||||
<div className="relative w-20 h-20 mx-auto mb-3">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className={`text-2xl font-bold ${getScoreColor(completionData.overallScore)}`}>
|
||||
{completionData.overallScore}
|
||||
</span>
|
||||
</div>
|
||||
<svg className="w-20 h-20 transform -rotate-90">
|
||||
<circle
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="32"
|
||||
stroke="currentColor"
|
||||
strokeWidth="6"
|
||||
fill="none"
|
||||
className="text-gray-200"
|
||||
/>
|
||||
<circle
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="32"
|
||||
stroke="currentColor"
|
||||
strokeWidth="6"
|
||||
fill="none"
|
||||
strokeDasharray={`${completionData.overallScore * 2.01} 201`}
|
||||
className="text-green-600"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-gray-700">Puntuación General</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-blue-600">{completionData.overallProgress}%</p>
|
||||
<p className="text-sm font-medium text-gray-700">Progreso Total</p>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{ width: `${completionData.overallProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-purple-600">
|
||||
{completionData.completedSteps}/{completionData.totalSteps}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-700">Secciones Completadas</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-orange-600">{completionData.estimatedTimeRemaining}</p>
|
||||
<p className="text-sm font-medium text-gray-700">Tiempo Restante</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
{['overview', 'sections', 'recommendations', 'readiness'].map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveSection(tab)}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeSection === tab
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{tab === 'overview' && 'Resumen General'}
|
||||
{tab === 'sections' && 'Revisión por Secciones'}
|
||||
{tab === 'recommendations' && 'Recomendaciones'}
|
||||
{tab === 'readiness' && 'Preparación para Lanzamiento'}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Content based on active section */}
|
||||
{activeSection === 'overview' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Estado por Secciones</h3>
|
||||
<div className="space-y-3">
|
||||
{sectionReview.map((section) => (
|
||||
<div key={section.id} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStatusIcon(section.status)}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">{section.title}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{section.recommendations.length > 0
|
||||
? `${section.recommendations.length} recomendación${section.recommendations.length > 1 ? 'es' : ''}`
|
||||
: 'Completado correctamente'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className={`text-sm font-medium ${getScoreColor(section.score)}`}>
|
||||
{section.score}%
|
||||
</p>
|
||||
<Badge variant={getStatusColor(section.status)}>
|
||||
{section.status === 'completed' ? 'Completado' :
|
||||
section.status === 'warning' ? 'Con observaciones' : 'Pendiente'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Próximos Pasos</h3>
|
||||
<div className="space-y-4">
|
||||
{overallRecommendations.slice(0, 3).map((rec, index) => (
|
||||
<div key={index} className="flex items-start space-x-3 p-3 border rounded-lg">
|
||||
<Badge variant={getPriorityColor(rec.priority)} className="mt-1">
|
||||
{rec.category}
|
||||
</Badge>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium text-gray-900">{rec.title}</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">{rec.description}</p>
|
||||
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-500">
|
||||
<span>⏱️ {rec.estimatedTime}</span>
|
||||
<span>💡 {rec.impact}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="w-4 h-4 text-gray-400 mt-1" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<Star className="w-5 h-5 text-blue-600" />
|
||||
<h4 className="font-medium text-blue-900">¡Excelente progreso!</h4>
|
||||
</div>
|
||||
<p className="text-sm text-blue-800">
|
||||
Has completado el 95% de la configuración. Solo quedan algunos detalles finales antes del lanzamiento.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'sections' && (
|
||||
<div className="space-y-6">
|
||||
{sectionReview.map((section) => (
|
||||
<Card key={section.id} className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStatusIcon(section.status)}
|
||||
<h3 className="text-lg font-semibold text-gray-900">{section.title}</h3>
|
||||
<Badge variant={getStatusColor(section.status)}>
|
||||
Puntuación: {section.score}%
|
||||
</Badge>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Ver Detalles
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
{section.items.map((item, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
{getItemStatusIcon(item.status)}
|
||||
<span className="text-sm font-medium text-gray-700">{item.field}</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-600">{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{section.recommendations.length > 0 && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<h4 className="text-sm font-medium text-yellow-800 mb-2">Recomendaciones:</h4>
|
||||
<ul className="text-sm text-yellow-700 space-y-1">
|
||||
{section.recommendations.map((rec, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<span className="mr-2">•</span>
|
||||
<span>{rec}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'recommendations' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Todas las Recomendaciones</h3>
|
||||
<div className="space-y-4">
|
||||
{overallRecommendations.map((rec, index) => (
|
||||
<div key={index} className="flex items-start justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-start space-x-3 flex-1">
|
||||
<Badge variant={getPriorityColor(rec.priority)}>
|
||||
{rec.category}
|
||||
</Badge>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-1">{rec.title}</h4>
|
||||
<p className="text-sm text-gray-600 mb-2">{rec.description}</p>
|
||||
<div className="flex items-center space-x-4 text-xs text-gray-500">
|
||||
<span>⏱️ Tiempo estimado: {rec.estimatedTime}</span>
|
||||
<span>💡 Impacto: {rec.impact}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" variant="outline">Más Información</Button>
|
||||
<Button size="sm">Completar</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeSection === 'readiness' && (
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Preparación para el Lanzamiento</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<div className="w-16 h-16 mx-auto mb-3 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h4 className="font-medium text-gray-900 mb-2">Elementos Esenciales</h4>
|
||||
<p className="text-2xl font-bold text-green-600 mb-1">
|
||||
{launchReadiness.essential.completed}/{launchReadiness.essential.total}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{launchReadiness.essential.percentage}% completado</p>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-green-600 h-2 rounded-full"
|
||||
style={{ width: `${launchReadiness.essential.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<div className="w-16 h-16 mx-auto mb-3 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||
<Star className="w-8 h-8 text-yellow-600" />
|
||||
</div>
|
||||
<h4 className="font-medium text-gray-900 mb-2">Recomendados</h4>
|
||||
<p className="text-2xl font-bold text-yellow-600 mb-1">
|
||||
{launchReadiness.recommended.completed}/{launchReadiness.recommended.total}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{launchReadiness.recommended.percentage}% completado</p>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-yellow-600 h-2 rounded-full"
|
||||
style={{ width: `${launchReadiness.recommended.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<div className="w-16 h-16 mx-auto mb-3 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<Users className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<h4 className="font-medium text-gray-900 mb-2">Opcionales</h4>
|
||||
<p className="text-2xl font-bold text-blue-600 mb-1">
|
||||
{launchReadiness.optional.completed}/{launchReadiness.optional.total}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{launchReadiness.optional.percentage}% completado</p>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{ width: `${launchReadiness.optional.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-6">
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||
<h4 className="text-lg font-semibold text-green-900">¡Listo para Lanzar!</h4>
|
||||
</div>
|
||||
<p className="text-green-800 mb-4">
|
||||
Tu configuración está lista para el lanzamiento. Todos los elementos esenciales están completados
|
||||
y el sistema está preparado para comenzar a operar.
|
||||
</p>
|
||||
<div className="flex space-x-3">
|
||||
<Button className="bg-green-600 hover:bg-green-700">
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
Lanzar Ahora
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
Ejecutar Pruebas Finales
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingReviewPage;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as OnboardingReviewPage } from './OnboardingReviewPage';
|
||||
@@ -1,906 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { ChevronRight, ChevronLeft, Check, Store, Upload, Brain } from 'lucide-react';
|
||||
import { Button, Card, Input } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const OnboardingSetupPage: React.FC = () => {
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [formData, setFormData] = useState({
|
||||
bakery: {
|
||||
name: 'Panadería Artesanal El Buen Pan',
|
||||
type: 'artisan',
|
||||
size: 'medium',
|
||||
location: 'Av. Principal 123, Centro Histórico',
|
||||
phone: '+1 234 567 8900',
|
||||
email: 'info@elbuenpan.com'
|
||||
},
|
||||
upload: {
|
||||
salesData: null,
|
||||
inventoryData: null,
|
||||
recipeData: null
|
||||
}
|
||||
});
|
||||
|
||||
const [trainingState, setTrainingState] = useState({
|
||||
isTraining: false,
|
||||
progress: 0,
|
||||
currentStep: 'Iniciando entrenamiento...',
|
||||
status: 'pending', // 'pending', 'running', 'completed', 'error'
|
||||
logs: [] as string[]
|
||||
});
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Información de la Panadería',
|
||||
description: 'Detalles básicos sobre tu negocio',
|
||||
icon: Store,
|
||||
fields: ['name', 'type', 'location', 'contact']
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Carga de Datos',
|
||||
description: 'Importa tus datos existentes para acelerar la configuración inicial',
|
||||
icon: Upload,
|
||||
fields: ['files', 'templates', 'validation']
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Entrenamiento IA',
|
||||
description: 'Configurando tu modelo de inteligencia artificial personalizado',
|
||||
icon: Brain,
|
||||
fields: ['training', 'progress', 'completion']
|
||||
}
|
||||
];
|
||||
|
||||
const bakeryTypes = [
|
||||
{
|
||||
value: 'artisan',
|
||||
label: 'Panadería Artesanal Local',
|
||||
description: 'Producción propia y tradicional en el local'
|
||||
},
|
||||
{
|
||||
value: 'dependent',
|
||||
label: 'Panadería Dependiente',
|
||||
description: 'Dependiente de un panadero central'
|
||||
}
|
||||
];
|
||||
|
||||
const handleInputChange = (section: string, field: string, value: any) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section as keyof typeof prev],
|
||||
[field]: value
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const handleArrayToggle = (section: string, field: string, value: string) => {
|
||||
setFormData(prev => {
|
||||
const currentArray = prev[section as keyof typeof prev][field] || [];
|
||||
const newArray = currentArray.includes(value)
|
||||
? currentArray.filter((item: string) => item !== value)
|
||||
: [...currentArray, value];
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section as keyof typeof prev],
|
||||
[field]: newArray
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const nextStep = () => {
|
||||
if (currentStep < steps.length) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinish = () => {
|
||||
console.log('Onboarding completed:', formData);
|
||||
// Navigate to dashboard - onboarding is complete
|
||||
window.location.href = '/app/dashboard';
|
||||
};
|
||||
|
||||
const downloadTemplate = (type: 'sales' | 'inventory' | 'recipes') => {
|
||||
let csvContent = '';
|
||||
let fileName = '';
|
||||
|
||||
switch (type) {
|
||||
case 'sales':
|
||||
csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
|
||||
2024-01-15,Pan Integral,5,2.50,12.50,Cliente A,Tienda
|
||||
2024-01-15,Croissant,3,1.80,5.40,Cliente B,Online
|
||||
2024-01-15,Baguette,2,3.00,6.00,Cliente C,Tienda
|
||||
2024-01-16,Pan de Centeno,4,2.80,11.20,Cliente A,Tienda
|
||||
2024-01-16,Empanadas,6,4.50,27.00,Cliente D,Delivery`;
|
||||
fileName = 'plantilla_ventas.csv';
|
||||
break;
|
||||
case 'inventory':
|
||||
csvContent = `nombre,categoria,unidad_medida,stock_actual,stock_minimo,stock_maximo,precio_compra,proveedor,fecha_vencimiento
|
||||
Harina de Trigo,Ingrediente,kg,50,20,100,1.20,Molinos del Sur,2024-12-31
|
||||
Azúcar Blanca,Ingrediente,kg,25,10,50,0.85,Dulces SA,2024-12-31
|
||||
Levadura Fresca,Ingrediente,kg,5,2,10,3.50,Levaduras Pro,2024-03-15
|
||||
Mantequilla,Ingrediente,kg,15,5,30,4.20,Lácteos Premium,2024-02-28
|
||||
Pan Integral,Producto Final,unidad,20,10,50,0.00,Producción Propia,2024-01-20`;
|
||||
fileName = 'plantilla_inventario.csv';
|
||||
break;
|
||||
case 'recipes':
|
||||
csvContent = `nombre_receta,categoria,tiempo_preparacion,tiempo_coccion,porciones,ingrediente,cantidad,unidad
|
||||
Pan Integral,Panadería,30,45,2,Harina Integral,500,g
|
||||
Pan Integral,Panadería,30,45,2,Agua,325,ml
|
||||
Pan Integral,Panadería,30,45,2,Sal,10,g
|
||||
Pan Integral,Panadería,30,45,2,Levadura,7,g
|
||||
Croissant,Bollería,120,20,12,Harina,400,g
|
||||
Croissant,Bollería,120,20,12,Mantequilla,250,g
|
||||
Croissant,Bollería,120,20,12,Agua,200,ml
|
||||
Croissant,Bollería,120,20,12,Sal,8,g`;
|
||||
fileName = 'plantilla_recetas.csv';
|
||||
break;
|
||||
}
|
||||
|
||||
// Create and download the file
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', fileName);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
// WebSocket connection for ML training updates
|
||||
const connectWebSocket = () => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wsUrl = process.env.NODE_ENV === 'production'
|
||||
? 'wss://api.bakeryai.com/ws/training'
|
||||
: 'ws://localhost:8000/ws/training';
|
||||
|
||||
wsRef.current = new WebSocket(wsUrl);
|
||||
|
||||
wsRef.current.onopen = () => {
|
||||
console.log('WebSocket connected for ML training');
|
||||
setTrainingState(prev => ({ ...prev, status: 'running' }));
|
||||
};
|
||||
|
||||
wsRef.current.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
setTrainingState(prev => ({
|
||||
...prev,
|
||||
progress: data.progress || prev.progress,
|
||||
currentStep: data.step || prev.currentStep,
|
||||
status: data.status || prev.status,
|
||||
logs: data.log ? [...prev.logs, data.log] : prev.logs
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
wsRef.current.onclose = () => {
|
||||
console.log('WebSocket connection closed');
|
||||
};
|
||||
|
||||
wsRef.current.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
setTrainingState(prev => ({ ...prev, status: 'error' }));
|
||||
};
|
||||
};
|
||||
|
||||
const startTraining = () => {
|
||||
setTrainingState(prev => ({ ...prev, isTraining: true, progress: 0 }));
|
||||
connectWebSocket();
|
||||
|
||||
// Send training configuration to server
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(JSON.stringify({
|
||||
action: 'start_training',
|
||||
bakery_data: formData.bakery,
|
||||
upload_data: formData.upload
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup WebSocket on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isStepComplete = (stepId: number) => {
|
||||
// Basic validation logic
|
||||
switch (stepId) {
|
||||
case 1:
|
||||
return formData.bakery.name && formData.bakery.location;
|
||||
case 2:
|
||||
return true; // Upload step is now optional - users can skip if they want
|
||||
case 3:
|
||||
return trainingState.status === 'completed';
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-3">
|
||||
<label className="block text-lg font-semibold text-[var(--text-primary)] mb-3">
|
||||
Nombre de la Panadería *
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={formData.bakery.name}
|
||||
onChange={(e) => handleInputChange('bakery', 'name', e.target.value)}
|
||||
placeholder="Ej: Panadería Artesanal El Buen Pan"
|
||||
className="w-full transition-all duration-300 focus:scale-[1.02] focus:shadow-lg"
|
||||
/>
|
||||
{formData.bakery.name && (
|
||||
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<Check className="w-5 h-5 text-[var(--color-success)]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-lg font-semibold text-[var(--text-primary)] mb-4">
|
||||
Tipo de Panadería *
|
||||
</label>
|
||||
<div className="space-y-4">
|
||||
{bakeryTypes.map((type) => (
|
||||
<label
|
||||
key={type.value}
|
||||
className={`
|
||||
group relative flex p-6 border-2 rounded-xl cursor-pointer transition-all duration-300 hover:scale-[1.02] hover:shadow-lg
|
||||
${formData.bakery.type === type.value
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5 shadow-lg'
|
||||
: 'border-[var(--border-secondary)] bg-[var(--bg-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-tertiary)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start space-x-4 w-full">
|
||||
<div className="relative flex-shrink-0 mt-1">
|
||||
<input
|
||||
type="radio"
|
||||
name="bakeryType"
|
||||
value={type.value}
|
||||
checked={formData.bakery.type === type.value}
|
||||
onChange={(e) => handleInputChange('bakery', 'type', e.target.value)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`
|
||||
w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all duration-300
|
||||
${formData.bakery.type === type.value
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]'
|
||||
: 'border-[var(--border-tertiary)] group-hover:border-[var(--color-primary)]'
|
||||
}
|
||||
`}>
|
||||
{formData.bakery.type === type.value && (
|
||||
<div className="w-3 h-3 bg-white rounded-full"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className={`
|
||||
text-lg font-semibold mb-2 transition-colors duration-300
|
||||
${formData.bakery.type === type.value ? 'text-[var(--text-primary)]' : 'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]'}
|
||||
`}>
|
||||
{type.label}
|
||||
</h3>
|
||||
<p className={`text-sm transition-colors duration-300 ${
|
||||
formData.bakery.type === type.value ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)] group-hover:text-[var(--text-secondary)]'
|
||||
}`}>
|
||||
{type.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selection indicator */}
|
||||
{formData.bakery.type === type.value && (
|
||||
<div className="absolute top-4 right-4">
|
||||
<Check className="w-5 h-5 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Ubicación *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.bakery.location}
|
||||
onChange={(e) => handleInputChange('bakery', 'location', e.target.value)}
|
||||
placeholder="Dirección completa"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Teléfono
|
||||
</label>
|
||||
<Input
|
||||
value={formData.bakery.phone}
|
||||
onChange={(e) => handleInputChange('bakery', 'phone', e.target.value)}
|
||||
placeholder="+34 xxx xxx xxx"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
value={formData.bakery.email}
|
||||
onChange={(e) => handleInputChange('bakery', 'email', e.target.value)}
|
||||
placeholder="contacto@panaderia.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 2:
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Upload Options */}
|
||||
<div className="space-y-4">
|
||||
<div className={`grid grid-cols-1 gap-4 ${
|
||||
formData.bakery.type === 'dependent' ? 'md:grid-cols-2' : 'md:grid-cols-3'
|
||||
}`}>
|
||||
{/* Sales Data Upload */}
|
||||
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-6 text-center transition-all duration-300 hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)]/5">
|
||||
<div className="w-12 h-12 bg-[var(--color-success)]/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-6 h-6 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">Datos de Ventas</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-3">
|
||||
Historial de ventas (CSV, Excel)
|
||||
</p>
|
||||
<div className="text-xs text-[var(--text-tertiary)] mb-4 bg-[var(--bg-secondary)] rounded-lg p-3 text-left">
|
||||
<div className="font-medium mb-1">Columnas requeridas:</div>
|
||||
<div>• <strong>Fecha</strong> (date, fecha)</div>
|
||||
<div>• <strong>Producto</strong> (product, producto)</div>
|
||||
<div className="font-medium mt-2 mb-1">Columnas opcionales:</div>
|
||||
<div>• Cantidad, Precio, Categoría, Ubicación</div>
|
||||
<div className="mt-2 text-[var(--color-info)]">Formatos: CSV, Excel | Máx: 10MB</div>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,.xlsx,.xls"
|
||||
onChange={(e) => handleInputChange('upload', 'salesData', e.target.files?.[0] || null)}
|
||||
className="hidden"
|
||||
id="sales-upload"
|
||||
/>
|
||||
<label htmlFor="sales-upload" className="btn btn-outline text-sm cursor-pointer">
|
||||
{formData.upload.salesData ? 'Archivo cargado ✓' : 'Seleccionar archivo'}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Inventory Data Upload */}
|
||||
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-6 text-center transition-all duration-300 hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)]/5">
|
||||
<div className="w-12 h-12 bg-[var(--color-warning)]/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-6 h-6 text-[var(--color-warning)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">Inventario</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||
Lista de productos e ingredientes
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,.xlsx,.xls"
|
||||
onChange={(e) => handleInputChange('upload', 'inventoryData', e.target.files?.[0] || null)}
|
||||
className="hidden"
|
||||
id="inventory-upload"
|
||||
/>
|
||||
<label htmlFor="inventory-upload" className="btn btn-outline text-sm cursor-pointer">
|
||||
{formData.upload.inventoryData ? 'Archivo cargado ✓' : 'Seleccionar archivo'}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Recipe Data Upload - Only for artisan bakeries */}
|
||||
{formData.bakery.type === 'artisan' && (
|
||||
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-6 text-center transition-all duration-300 hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)]/5">
|
||||
<div className="w-12 h-12 bg-[var(--color-info)]/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-6 h-6 text-[var(--color-info)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">Recetas</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||
Recetas y fórmulas existentes
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,.xlsx,.xls,.pdf"
|
||||
onChange={(e) => handleInputChange('upload', 'recipeData', e.target.files?.[0] || null)}
|
||||
className="hidden"
|
||||
id="recipe-upload"
|
||||
/>
|
||||
<label htmlFor="recipe-upload" className="btn btn-outline text-sm cursor-pointer">
|
||||
{formData.upload.recipeData ? 'Archivo cargado ✓' : 'Seleccionar archivo'}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Template Downloads */}
|
||||
<div className="space-y-4 pt-6 border-t border-[var(--border-secondary)]">
|
||||
<div className="text-center">
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">¿Necesitas ayuda con el formato?</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||
Descarga nuestras plantillas para estructurar correctamente tus datos
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<button
|
||||
onClick={() => downloadTemplate('sales')}
|
||||
className="flex items-center justify-center px-4 py-2 border border-[var(--color-success)] text-[var(--color-success)] rounded-lg hover:bg-[var(--color-success)]/10 transition-all duration-200"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Plantilla de Ventas
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => downloadTemplate('inventory')}
|
||||
className="flex items-center justify-center px-4 py-2 border border-[var(--color-warning)] text-[var(--color-warning)] rounded-lg hover:bg-[var(--color-warning)]/10 transition-all duration-200"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Plantilla de Inventario
|
||||
</button>
|
||||
|
||||
{/* Recipe template - Only for artisan bakeries */}
|
||||
{formData.bakery.type === 'artisan' && (
|
||||
<button
|
||||
onClick={() => downloadTemplate('recipes')}
|
||||
className="flex items-center justify-center px-4 py-2 border border-[var(--color-info)] text-[var(--color-info)] rounded-lg hover:bg-[var(--color-info)]/10 transition-all duration-200"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Plantilla de Recetas
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 3:
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Training Progress */}
|
||||
{!trainingState.isTraining && trainingState.status === 'pending' && (
|
||||
<div className="text-center">
|
||||
<p className="text-[var(--text-secondary)] mb-6">
|
||||
Presiona el botón para iniciar el entrenamiento de tu modelo de IA
|
||||
</p>
|
||||
<Button
|
||||
onClick={startTraining}
|
||||
className="px-8 py-3 bg-[var(--color-info)] hover:bg-[var(--color-info)]/80 text-white"
|
||||
>
|
||||
<Brain className="w-4 h-4 mr-2" />
|
||||
Iniciar Entrenamiento
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Training in Progress */}
|
||||
{trainingState.isTraining && (
|
||||
<div className="space-y-6">
|
||||
{/* Progress Bar */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
Progreso del Entrenamiento
|
||||
</span>
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
{trainingState.progress}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-4 overflow-hidden">
|
||||
<div
|
||||
className="bg-gradient-to-r from-[var(--color-info)] to-[var(--color-primary)] h-full rounded-full transition-all duration-500 ease-out relative"
|
||||
style={{ width: `${trainingState.progress}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-white opacity-20 rounded-full"></div>
|
||||
{trainingState.progress > 0 && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-pulse rounded-full"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Step */}
|
||||
<div className="flex items-center space-x-3 p-4 bg-[var(--bg-secondary)] rounded-lg border">
|
||||
<div className="w-3 h-3 bg-[var(--color-info)] rounded-full animate-pulse"></div>
|
||||
<span className="text-[var(--text-primary)] font-medium">
|
||||
{trainingState.currentStep}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Training Status */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className={`p-4 rounded-lg border text-center ${
|
||||
trainingState.progress >= 25
|
||||
? 'bg-[var(--color-success)]/10 border-[var(--color-success)]'
|
||||
: 'bg-[var(--bg-secondary)] border-[var(--border-secondary)]'
|
||||
}`}>
|
||||
<div className={`w-8 h-8 mx-auto mb-2 rounded-full flex items-center justify-center ${
|
||||
trainingState.progress >= 25 ? 'bg-[var(--color-success)]' : 'bg-[var(--bg-tertiary)]'
|
||||
}`}>
|
||||
{trainingState.progress >= 25 ? (
|
||||
<Check className="w-4 h-4 text-white" />
|
||||
) : (
|
||||
<span className="text-xs text-[var(--text-tertiary)]">1</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs font-medium">Carga de Datos</p>
|
||||
</div>
|
||||
|
||||
<div className={`p-4 rounded-lg border text-center ${
|
||||
trainingState.progress >= 75
|
||||
? 'bg-[var(--color-success)]/10 border-[var(--color-success)]'
|
||||
: 'bg-[var(--bg-secondary)] border-[var(--border-secondary)]'
|
||||
}`}>
|
||||
<div className={`w-8 h-8 mx-auto mb-2 rounded-full flex items-center justify-center ${
|
||||
trainingState.progress >= 75 ? 'bg-[var(--color-success)]' : 'bg-[var(--bg-tertiary)]'
|
||||
}`}>
|
||||
{trainingState.progress >= 75 ? (
|
||||
<Check className="w-4 h-4 text-white" />
|
||||
) : (
|
||||
<span className="text-xs text-[var(--text-tertiary)]">2</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs font-medium">Entrenamiento</p>
|
||||
</div>
|
||||
|
||||
<div className={`p-4 rounded-lg border text-center ${
|
||||
trainingState.status === 'completed'
|
||||
? 'bg-[var(--color-success)]/10 border-[var(--color-success)]'
|
||||
: 'bg-[var(--bg-secondary)] border-[var(--border-secondary)]'
|
||||
}`}>
|
||||
<div className={`w-8 h-8 mx-auto mb-2 rounded-full flex items-center justify-center ${
|
||||
trainingState.status === 'completed' ? 'bg-[var(--color-success)]' : 'bg-[var(--bg-tertiary)]'
|
||||
}`}>
|
||||
{trainingState.status === 'completed' ? (
|
||||
<Check className="w-4 h-4 text-white" />
|
||||
) : (
|
||||
<span className="text-xs text-[var(--text-tertiary)]">3</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs font-medium">Validación</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Training Logs */}
|
||||
{trainingState.logs.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
|
||||
Log de Entrenamiento
|
||||
</h4>
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 max-h-32 overflow-y-auto border">
|
||||
{trainingState.logs.slice(-5).map((log, index) => (
|
||||
<div key={index} className="text-xs text-[var(--text-secondary)] mb-1 font-mono">
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Training Completed */}
|
||||
{trainingState.status === 'completed' && (
|
||||
<div className="text-center p-6 bg-[var(--color-success)]/10 rounded-xl border border-[var(--color-success)]/20">
|
||||
<Check className="w-12 h-12 text-[var(--color-success)] mx-auto mb-4" />
|
||||
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
¡Entrenamiento Completado!
|
||||
</h4>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
Tu modelo de IA personalizado está listo para ayudarte a optimizar tu panadería
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Training Error */}
|
||||
{trainingState.status === 'error' && (
|
||||
<div className="text-center p-6 bg-[var(--color-error)]/10 rounded-xl border border-[var(--color-error)]/20">
|
||||
<div className="w-12 h-12 bg-[var(--color-error)] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span className="text-white text-xl">!</span>
|
||||
</div>
|
||||
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
Error en el Entrenamiento
|
||||
</h4>
|
||||
<p className="text-[var(--text-secondary)] mb-4">
|
||||
Ocurrió un problema durante el entrenamiento. Por favor, inténtalo de nuevo.
|
||||
</p>
|
||||
<Button
|
||||
onClick={startTraining}
|
||||
className="px-6 py-2 bg-[var(--color-info)] hover:bg-[var(--color-info)]/80 text-white"
|
||||
>
|
||||
Reintentar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 sm:p-6 max-w-5xl mx-auto">
|
||||
<div className="text-center mb-8 sm:mb-12">
|
||||
<div className="inline-flex items-center justify-center w-14 h-14 sm:w-16 sm:h-16 bg-[var(--color-primary)] rounded-full mb-4 sm:mb-6 shadow-lg">
|
||||
<svg className="w-6 h-6 sm:w-8 sm:h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] mb-3 sm:mb-4">
|
||||
Configuración Inicial
|
||||
</h1>
|
||||
<p className="text-base sm:text-lg text-[var(--text-secondary)] max-w-2xl mx-auto px-4">
|
||||
Configura tu panadería paso a paso para comenzar a usar la plataforma de manera óptima
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Single Integrated Card */}
|
||||
<Card className="shadow-lg overflow-hidden">
|
||||
{/* Progress Header Inside Card */}
|
||||
<div className="bg-[var(--bg-secondary)] p-4 sm:p-6 border-b border-[var(--border-secondary)]">
|
||||
{/* Step Indicators - Mobile Optimized */}
|
||||
<div className="mb-4 sm:mb-6">
|
||||
{/* Mobile: Vertical Step List */}
|
||||
<div className="block sm:hidden space-y-3">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.id} className="flex items-center space-x-3">
|
||||
<div className={`
|
||||
w-8 h-8 rounded-full flex items-center justify-center text-xs font-medium transition-all duration-300 flex-shrink-0
|
||||
${step.id <= currentStep
|
||||
? 'bg-[var(--color-primary)] text-white'
|
||||
: 'bg-[var(--bg-quaternary)] text-[var(--text-tertiary)] border border-[var(--border-secondary)]'
|
||||
}
|
||||
`}>
|
||||
{step.id < currentStep ? (
|
||||
<Check className="w-3 h-3" />
|
||||
) : step.id === currentStep ? (
|
||||
<step.icon className="w-3 h-3" />
|
||||
) : (
|
||||
<step.icon className="w-3 h-3" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className={`text-sm font-medium ${
|
||||
step.id === currentStep
|
||||
? 'text-[var(--color-primary)]'
|
||||
: step.id < currentStep
|
||||
? 'text-[var(--color-success)]'
|
||||
: 'text-[var(--text-tertiary)]'
|
||||
}`}>
|
||||
{step.title}
|
||||
</h3>
|
||||
{step.id === currentStep && (
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
{step.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Desktop: Horizontal Step Indicators */}
|
||||
<div className="hidden sm:flex items-center justify-center space-x-6">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.id} className="flex items-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`
|
||||
w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-all duration-300 shadow-sm
|
||||
${step.id <= currentStep
|
||||
? 'bg-[var(--color-primary)] text-white'
|
||||
: 'bg-[var(--bg-quaternary)] text-[var(--text-tertiary)] border border-[var(--border-secondary)]'
|
||||
}
|
||||
`}>
|
||||
{step.id < currentStep ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : step.id === currentStep ? (
|
||||
<step.icon className="w-4 h-4" />
|
||||
) : (
|
||||
<step.icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<p className={`text-xs mt-2 text-center max-w-16 leading-tight ${
|
||||
step.id === currentStep
|
||||
? 'text-[var(--color-primary)] font-semibold'
|
||||
: 'text-[var(--text-tertiary)]'
|
||||
}`}>
|
||||
{step.title}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Connection line - Desktop only */}
|
||||
{index < steps.length - 1 && (
|
||||
<div className={`
|
||||
w-16 h-0.5 mx-3 transition-all duration-500
|
||||
${step.id < currentStep ? 'bg-[var(--color-primary)]' : 'bg-[var(--border-secondary)]'}
|
||||
`} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step Info - Desktop Only (Mobile shows inline) */}
|
||||
<div className="text-center hidden sm:block">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-2">
|
||||
Paso {currentStep}: {steps[currentStep - 1].title}
|
||||
</h2>
|
||||
<p className="text-[var(--text-secondary)] mb-4">
|
||||
{steps[currentStep - 1].description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Mobile Step Info */}
|
||||
<div className="text-center block sm:hidden mb-4">
|
||||
<h2 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
{steps[currentStep - 1].title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="relative max-w-sm sm:max-w-md mx-auto">
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className="bg-[var(--color-primary)] h-full rounded-full transition-all duration-700 ease-out"
|
||||
style={{ width: `${(currentStep / steps.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center mt-2">
|
||||
<span className="text-sm font-medium text-[var(--color-primary)]">
|
||||
{Math.round((currentStep / steps.length) * 100)}% Completado
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Content */}
|
||||
<div className="p-4 sm:p-8">
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{renderStepContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Footer */}
|
||||
<div className="bg-[var(--bg-secondary)] px-4 sm:px-8 py-4 sm:py-6 border-t border-[var(--border-secondary)]">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 sm:gap-0">
|
||||
{/* Mobile: Full width buttons stacked */}
|
||||
<div className="flex w-full sm:w-auto gap-3 sm:hidden">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={prevStep}
|
||||
disabled={currentStep === 1}
|
||||
className="flex-1 py-3 disabled:opacity-50"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 mr-2" />
|
||||
Anterior
|
||||
</Button>
|
||||
|
||||
{currentStep === steps.length ? (
|
||||
<Button
|
||||
onClick={handleFinish}
|
||||
disabled={!isStepComplete(currentStep)}
|
||||
className="flex-1 py-3 bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white disabled:opacity-50"
|
||||
>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Finalizar
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={nextStep}
|
||||
disabled={!isStepComplete(currentStep)}
|
||||
className="flex-1 py-3 disabled:opacity-50"
|
||||
>
|
||||
Siguiente
|
||||
<ChevronRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop: Original layout */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={prevStep}
|
||||
disabled={currentStep === 1}
|
||||
className="hidden sm:flex px-6 py-3 disabled:opacity-50"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 mr-2" />
|
||||
Anterior
|
||||
</Button>
|
||||
|
||||
{/* Step indicators - smaller on mobile */}
|
||||
<div className="flex items-center space-x-1.5 sm:space-x-2">
|
||||
{steps.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`
|
||||
w-1.5 h-1.5 sm:w-2 sm:h-2 rounded-full transition-all duration-300
|
||||
${index + 1 === currentStep
|
||||
? 'bg-[var(--color-primary)] scale-125'
|
||||
: index + 1 < currentStep
|
||||
? 'bg-[var(--color-success)]'
|
||||
: 'bg-[var(--border-secondary)]'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{currentStep === steps.length ? (
|
||||
<Button
|
||||
onClick={handleFinish}
|
||||
disabled={!isStepComplete(currentStep)}
|
||||
className="hidden sm:flex px-8 py-3 bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white disabled:opacity-50"
|
||||
>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Finalizar
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={nextStep}
|
||||
disabled={!isStepComplete(currentStep)}
|
||||
className="hidden sm:flex px-6 py-3 disabled:opacity-50"
|
||||
>
|
||||
Siguiente
|
||||
<ChevronRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingSetupPage;
|
||||
@@ -1,499 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ChevronRight, ChevronLeft, Check, Store, Users, Settings, Zap } from 'lucide-react';
|
||||
import { Button, Card, Input } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const OnboardingSetupPage: React.FC = () => {
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [formData, setFormData] = useState({
|
||||
bakery: {
|
||||
name: '',
|
||||
type: 'traditional',
|
||||
size: 'medium',
|
||||
location: '',
|
||||
phone: '',
|
||||
email: ''
|
||||
},
|
||||
team: {
|
||||
ownerName: '',
|
||||
teamSize: '5-10',
|
||||
roles: [],
|
||||
experience: 'intermediate'
|
||||
},
|
||||
operations: {
|
||||
openingHours: {
|
||||
start: '07:00',
|
||||
end: '20:00'
|
||||
},
|
||||
daysOpen: 6,
|
||||
specialties: [],
|
||||
dailyProduction: 'medium'
|
||||
},
|
||||
goals: {
|
||||
primaryGoals: [],
|
||||
expectedRevenue: '',
|
||||
timeline: '6months'
|
||||
}
|
||||
});
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Información de la Panadería',
|
||||
description: 'Detalles básicos sobre tu negocio',
|
||||
icon: Store,
|
||||
fields: ['name', 'type', 'location', 'contact']
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Equipo y Personal',
|
||||
description: 'Información sobre tu equipo de trabajo',
|
||||
icon: Users,
|
||||
fields: ['owner', 'teamSize', 'roles', 'experience']
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Operaciones',
|
||||
description: 'Horarios y especialidades de producción',
|
||||
icon: Settings,
|
||||
fields: ['hours', 'specialties', 'production']
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Objetivos',
|
||||
description: 'Metas y expectativas para tu panadería',
|
||||
icon: Zap,
|
||||
fields: ['goals', 'revenue', 'timeline']
|
||||
}
|
||||
];
|
||||
|
||||
const bakeryTypes = [
|
||||
{ value: 'traditional', label: 'Panadería Tradicional' },
|
||||
{ value: 'artisan', label: 'Panadería Artesanal' },
|
||||
{ value: 'cafe', label: 'Panadería-Café' },
|
||||
{ value: 'industrial', label: 'Producción Industrial' }
|
||||
];
|
||||
|
||||
const specialties = [
|
||||
{ value: 'bread', label: 'Pan Tradicional' },
|
||||
{ value: 'pastries', label: 'Bollería' },
|
||||
{ value: 'cakes', label: 'Tartas y Pasteles' },
|
||||
{ value: 'cookies', label: 'Galletas' },
|
||||
{ value: 'savory', label: 'Productos Salados' },
|
||||
{ value: 'gluten-free', label: 'Sin Gluten' },
|
||||
{ value: 'vegan', label: 'Vegano' },
|
||||
{ value: 'organic', label: 'Orgánico' }
|
||||
];
|
||||
|
||||
const businessGoals = [
|
||||
{ value: 'increase-sales', label: 'Aumentar Ventas' },
|
||||
{ value: 'reduce-waste', label: 'Reducir Desperdicios' },
|
||||
{ value: 'improve-efficiency', label: 'Mejorar Eficiencia' },
|
||||
{ value: 'expand-menu', label: 'Ampliar Menú' },
|
||||
{ value: 'digital-presence', label: 'Presencia Digital' },
|
||||
{ value: 'customer-loyalty', label: 'Fidelización de Clientes' }
|
||||
];
|
||||
|
||||
const handleInputChange = (section: string, field: string, value: any) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section as keyof typeof prev],
|
||||
[field]: value
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const handleArrayToggle = (section: string, field: string, value: string) => {
|
||||
setFormData(prev => {
|
||||
const currentArray = prev[section as keyof typeof prev][field] || [];
|
||||
const newArray = currentArray.includes(value)
|
||||
? currentArray.filter((item: string) => item !== value)
|
||||
: [...currentArray, value];
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section as keyof typeof prev],
|
||||
[field]: newArray
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const nextStep = () => {
|
||||
if (currentStep < steps.length) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinish = () => {
|
||||
console.log('Onboarding completed:', formData);
|
||||
// Handle completion logic
|
||||
};
|
||||
|
||||
const isStepComplete = (stepId: number) => {
|
||||
// Basic validation logic
|
||||
switch (stepId) {
|
||||
case 1:
|
||||
return formData.bakery.name && formData.bakery.location;
|
||||
case 2:
|
||||
return formData.team.ownerName;
|
||||
case 3:
|
||||
return formData.operations.specialties.length > 0;
|
||||
case 4:
|
||||
return formData.goals.primaryGoals.length > 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nombre de la Panadería *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.bakery.name}
|
||||
onChange={(e) => handleInputChange('bakery', 'name', e.target.value)}
|
||||
placeholder="Ej: Panadería San Miguel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Tipo de Panadería
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{bakeryTypes.map((type) => (
|
||||
<label key={type.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
||||
<input
|
||||
type="radio"
|
||||
name="bakeryType"
|
||||
value={type.value}
|
||||
checked={formData.bakery.type === type.value}
|
||||
onChange={(e) => handleInputChange('bakery', 'type', e.target.value)}
|
||||
className="text-blue-600"
|
||||
/>
|
||||
<span className="text-sm">{type.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ubicación *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.bakery.location}
|
||||
onChange={(e) => handleInputChange('bakery', 'location', e.target.value)}
|
||||
placeholder="Dirección completa"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Teléfono
|
||||
</label>
|
||||
<Input
|
||||
value={formData.bakery.phone}
|
||||
onChange={(e) => handleInputChange('bakery', 'phone', e.target.value)}
|
||||
placeholder="+34 xxx xxx xxx"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
value={formData.bakery.email}
|
||||
onChange={(e) => handleInputChange('bakery', 'email', e.target.value)}
|
||||
placeholder="contacto@panaderia.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 2:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nombre del Propietario *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.team.ownerName}
|
||||
onChange={(e) => handleInputChange('team', 'ownerName', e.target.value)}
|
||||
placeholder="Tu nombre completo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Tamaño del Equipo
|
||||
</label>
|
||||
<select
|
||||
value={formData.team.teamSize}
|
||||
onChange={(e) => handleInputChange('team', 'teamSize', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="1-2">Solo yo o 1-2 personas</option>
|
||||
<option value="3-5">3-5 empleados</option>
|
||||
<option value="5-10">5-10 empleados</option>
|
||||
<option value="10+">Más de 10 empleados</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Experiencia en el Sector
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ value: 'beginner', label: 'Principiante (menos de 2 años)' },
|
||||
{ value: 'intermediate', label: 'Intermedio (2-5 años)' },
|
||||
{ value: 'experienced', label: 'Experimentado (5-10 años)' },
|
||||
{ value: 'expert', label: 'Experto (más de 10 años)' }
|
||||
].map((exp) => (
|
||||
<label key={exp.value} className="flex items-center space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="experience"
|
||||
value={exp.value}
|
||||
checked={formData.team.experience === exp.value}
|
||||
onChange={(e) => handleInputChange('team', 'experience', e.target.value)}
|
||||
className="text-blue-600"
|
||||
/>
|
||||
<span className="text-sm">{exp.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 3:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Hora de Apertura
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={formData.operations.openingHours.start}
|
||||
onChange={(e) => handleInputChange('operations', 'openingHours', {
|
||||
...formData.operations.openingHours,
|
||||
start: e.target.value
|
||||
})}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Hora de Cierre
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={formData.operations.openingHours.end}
|
||||
onChange={(e) => handleInputChange('operations', 'openingHours', {
|
||||
...formData.operations.openingHours,
|
||||
end: e.target.value
|
||||
})}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Días de Operación por Semana
|
||||
</label>
|
||||
<select
|
||||
value={formData.operations.daysOpen}
|
||||
onChange={(e) => handleInputChange('operations', 'daysOpen', parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value={5}>5 días</option>
|
||||
<option value={6}>6 días</option>
|
||||
<option value={7}>7 días</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Especialidades *
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{specialties.map((specialty) => (
|
||||
<label key={specialty.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.operations.specialties.includes(specialty.value)}
|
||||
onChange={() => handleArrayToggle('operations', 'specialties', specialty.value)}
|
||||
className="text-blue-600 rounded"
|
||||
/>
|
||||
<span className="text-sm">{specialty.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 4:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Objetivos Principales *
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{businessGoals.map((goal) => (
|
||||
<label key={goal.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.goals.primaryGoals.includes(goal.value)}
|
||||
onChange={() => handleArrayToggle('goals', 'primaryGoals', goal.value)}
|
||||
className="text-blue-600 rounded"
|
||||
/>
|
||||
<span className="text-sm">{goal.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ingresos Mensuales Esperados (opcional)
|
||||
</label>
|
||||
<select
|
||||
value={formData.goals.expectedRevenue}
|
||||
onChange={(e) => handleInputChange('goals', 'expectedRevenue', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="">Seleccionar rango</option>
|
||||
<option value="0-5000">Menos de €5,000</option>
|
||||
<option value="5000-15000">€5,000 - €15,000</option>
|
||||
<option value="15000-30000">€15,000 - €30,000</option>
|
||||
<option value="30000+">Más de €30,000</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Plazo para Alcanzar Objetivos
|
||||
</label>
|
||||
<select
|
||||
value={formData.goals.timeline}
|
||||
onChange={(e) => handleInputChange('goals', 'timeline', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="3months">3 meses</option>
|
||||
<option value="6months">6 meses</option>
|
||||
<option value="1year">1 año</option>
|
||||
<option value="2years">2 años o más</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto">
|
||||
<PageHeader
|
||||
title="Configuración Inicial"
|
||||
description="Configura tu panadería paso a paso para comenzar"
|
||||
/>
|
||||
|
||||
{/* Progress Steps */}
|
||||
<Card className="p-6 mb-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.id} className="flex items-center">
|
||||
<div className={`flex items-center justify-center w-10 h-10 rounded-full ${
|
||||
step.id === currentStep
|
||||
? 'bg-blue-600 text-white'
|
||||
: step.id < currentStep || isStepComplete(step.id)
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
}`}>
|
||||
{step.id < currentStep || (step.id === currentStep && isStepComplete(step.id)) ? (
|
||||
<Check className="w-5 h-5" />
|
||||
) : (
|
||||
<step.icon className="w-5 h-5" />
|
||||
)}
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className={`w-full h-1 mx-4 ${
|
||||
step.id < currentStep ? 'bg-green-600' : 'bg-gray-200'
|
||||
}`} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Paso {currentStep}: {steps[currentStep - 1].title}
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
{steps[currentStep - 1].description}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Step Content */}
|
||||
<Card className="p-8 mb-8">
|
||||
{renderStepContent()}
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={prevStep}
|
||||
disabled={currentStep === 1}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 mr-2" />
|
||||
Anterior
|
||||
</Button>
|
||||
|
||||
{currentStep === steps.length ? (
|
||||
<Button onClick={handleFinish}>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Finalizar Configuración
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={nextStep}
|
||||
disabled={!isStepComplete(currentStep)}
|
||||
>
|
||||
Siguiente
|
||||
<ChevronRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingSetupPage;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as OnboardingSetupPage } from './OnboardingSetupPage';
|
||||
@@ -1,454 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Upload, File, Check, AlertCircle, Download, RefreshCw, Trash2, Eye } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const OnboardingUploadPage: React.FC = () => {
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const uploadedFiles = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'productos_menu.csv',
|
||||
type: 'productos',
|
||||
size: '45 KB',
|
||||
status: 'completed',
|
||||
uploadedAt: '2024-01-26 10:30:00',
|
||||
records: 127,
|
||||
errors: 3,
|
||||
warnings: 8
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'inventario_inicial.xlsx',
|
||||
type: 'inventario',
|
||||
size: '82 KB',
|
||||
status: 'completed',
|
||||
uploadedAt: '2024-01-26 10:25:00',
|
||||
records: 89,
|
||||
errors: 0,
|
||||
warnings: 2
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'empleados.csv',
|
||||
type: 'empleados',
|
||||
size: '12 KB',
|
||||
status: 'processing',
|
||||
uploadedAt: '2024-01-26 10:35:00',
|
||||
records: 8,
|
||||
errors: 0,
|
||||
warnings: 0
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'ventas_historicas.csv',
|
||||
type: 'ventas',
|
||||
size: '256 KB',
|
||||
status: 'error',
|
||||
uploadedAt: '2024-01-26 10:20:00',
|
||||
records: 0,
|
||||
errors: 1,
|
||||
warnings: 0,
|
||||
errorMessage: 'Formato de fecha incorrecto en la columna "fecha_venta"'
|
||||
}
|
||||
];
|
||||
|
||||
const supportedFormats = [
|
||||
{
|
||||
type: 'productos',
|
||||
name: 'Productos y Menú',
|
||||
formats: ['CSV', 'Excel'],
|
||||
description: 'Lista de productos con precios, categorías y descripciones',
|
||||
template: 'template_productos.csv',
|
||||
requiredFields: ['nombre', 'categoria', 'precio', 'descripcion']
|
||||
},
|
||||
{
|
||||
type: 'inventario',
|
||||
name: 'Inventario Inicial',
|
||||
formats: ['CSV', 'Excel'],
|
||||
description: 'Stock inicial de ingredientes y materias primas',
|
||||
template: 'template_inventario.xlsx',
|
||||
requiredFields: ['item', 'cantidad', 'unidad', 'costo_unitario']
|
||||
},
|
||||
{
|
||||
type: 'empleados',
|
||||
name: 'Empleados',
|
||||
formats: ['CSV'],
|
||||
description: 'Información del personal y roles',
|
||||
template: 'template_empleados.csv',
|
||||
requiredFields: ['nombre', 'cargo', 'email', 'telefono']
|
||||
},
|
||||
{
|
||||
type: 'ventas',
|
||||
name: 'Historial de Ventas',
|
||||
formats: ['CSV'],
|
||||
description: 'Datos históricos de ventas para análisis',
|
||||
template: 'template_ventas.csv',
|
||||
requiredFields: ['fecha', 'producto', 'cantidad', 'total']
|
||||
},
|
||||
{
|
||||
type: 'proveedores',
|
||||
name: 'Proveedores',
|
||||
formats: ['CSV', 'Excel'],
|
||||
description: 'Lista de proveedores y datos de contacto',
|
||||
template: 'template_proveedores.csv',
|
||||
requiredFields: ['nombre', 'contacto', 'telefono', 'email']
|
||||
}
|
||||
];
|
||||
|
||||
const uploadStats = {
|
||||
totalFiles: uploadedFiles.length,
|
||||
completedFiles: uploadedFiles.filter(f => f.status === 'completed').length,
|
||||
totalRecords: uploadedFiles.reduce((sum, f) => sum + f.records, 0),
|
||||
totalErrors: uploadedFiles.reduce((sum, f) => sum + f.errors, 0),
|
||||
totalWarnings: uploadedFiles.reduce((sum, f) => sum + f.warnings, 0)
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
const iconProps = { className: "w-5 h-5" };
|
||||
switch (status) {
|
||||
case 'completed': return <Check {...iconProps} className="w-5 h-5 text-[var(--color-success)]" />;
|
||||
case 'processing': return <RefreshCw {...iconProps} className="w-5 h-5 text-[var(--color-info)] animate-spin" />;
|
||||
case 'error': return <AlertCircle {...iconProps} className="w-5 h-5 text-[var(--color-error)]" />;
|
||||
default: return <File {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'green';
|
||||
case 'processing': return 'blue';
|
||||
case 'error': return 'red';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'Completado';
|
||||
case 'processing': return 'Procesando';
|
||||
case 'error': return 'Error';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragActive(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragActive(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragActive(false);
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
handleFiles(files);
|
||||
};
|
||||
|
||||
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
const files = Array.from(e.target.files);
|
||||
handleFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFiles = (files: File[]) => {
|
||||
console.log('Files selected:', files);
|
||||
// Simulate upload progress
|
||||
setIsProcessing(true);
|
||||
setUploadProgress(0);
|
||||
const interval = setInterval(() => {
|
||||
setUploadProgress(prev => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(interval);
|
||||
setIsProcessing(false);
|
||||
return 100;
|
||||
}
|
||||
return prev + 10;
|
||||
});
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const downloadTemplate = (template: string) => {
|
||||
console.log('Downloading template:', template);
|
||||
// Handle template download
|
||||
};
|
||||
|
||||
const retryUpload = (fileId: string) => {
|
||||
console.log('Retrying upload for file:', fileId);
|
||||
// Handle retry logic
|
||||
};
|
||||
|
||||
const deleteFile = (fileId: string) => {
|
||||
console.log('Deleting file:', fileId);
|
||||
// Handle delete logic
|
||||
};
|
||||
|
||||
const viewDetails = (fileId: string) => {
|
||||
console.log('Viewing details for file:', fileId);
|
||||
// Handle view details logic
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Carga de Datos"
|
||||
description="Importa tus datos existentes para acelerar la configuración inicial"
|
||||
/>
|
||||
|
||||
{/* Upload Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Archivos Subidos</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-info)]">{uploadStats.totalFiles}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
||||
<Upload className="h-6 w-6 text-[var(--color-info)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Completados</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-success)]">{uploadStats.completedFiles}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
||||
<Check className="h-6 w-6 text-[var(--color-success)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Registros</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{uploadStats.totalRecords}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<File className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Errores</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-error)]">{uploadStats.totalErrors}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
|
||||
<AlertCircle className="h-6 w-6 text-[var(--color-error)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Advertencias</p>
|
||||
<p className="text-3xl font-bold text-yellow-600">{uploadStats.totalWarnings}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||
<AlertCircle className="h-6 w-6 text-yellow-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Upload Area */}
|
||||
<Card className="p-8">
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
||||
dragActive
|
||||
? 'border-blue-400 bg-[var(--color-info)]/5'
|
||||
: 'border-[var(--border-secondary)] hover:border-[var(--border-tertiary)]'
|
||||
}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<Upload className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
Arrastra archivos aquí o haz clic para seleccionar
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] mb-4">
|
||||
Formatos soportados: CSV, Excel (XLSX). Tamaño máximo: 10MB por archivo
|
||||
</p>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
accept=".csv,.xlsx,.xls"
|
||||
onChange={handleFileInput}
|
||||
className="hidden"
|
||||
id="file-upload"
|
||||
/>
|
||||
<label htmlFor="file-upload">
|
||||
<Button className="cursor-pointer">
|
||||
Seleccionar Archivos
|
||||
</Button>
|
||||
</label>
|
||||
|
||||
{isProcessing && (
|
||||
<div className="mt-4">
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mb-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Procesando... {uploadProgress}%</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Supported Formats */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Formatos Soportados</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{supportedFormats.map((format, index) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-[var(--text-primary)]">{format.name}</h4>
|
||||
<div className="flex space-x-1">
|
||||
{format.formats.map((fmt, idx) => (
|
||||
<Badge key={idx} variant="blue" className="text-xs">{fmt}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-3">{format.description}</p>
|
||||
|
||||
<div className="mb-3">
|
||||
<p className="text-xs font-medium text-[var(--text-secondary)] mb-1">Campos requeridos:</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{format.requiredFields.map((field, idx) => (
|
||||
<span key={idx} className="px-2 py-1 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] text-xs rounded">
|
||||
{field}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => downloadTemplate(format.template)}
|
||||
>
|
||||
<Download className="w-3 h-3 mr-2" />
|
||||
Descargar Plantilla
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Uploaded Files */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Archivos Cargados</h3>
|
||||
<div className="space-y-3">
|
||||
{uploadedFiles.map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-center space-x-4 flex-1">
|
||||
{getStatusIcon(file.status)}
|
||||
|
||||
<div>
|
||||
<div className="flex items-center space-x-3 mb-1">
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)]">{file.name}</h4>
|
||||
<Badge variant={getStatusColor(file.status)}>
|
||||
{getStatusLabel(file.status)}
|
||||
</Badge>
|
||||
<span className="text-xs text-[var(--text-tertiary)]">{file.size}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4 text-xs text-[var(--text-secondary)]">
|
||||
<span>{file.records} registros</span>
|
||||
{file.errors > 0 && (
|
||||
<span className="text-[var(--color-error)]">{file.errors} errores</span>
|
||||
)}
|
||||
{file.warnings > 0 && (
|
||||
<span className="text-yellow-600">{file.warnings} advertencias</span>
|
||||
)}
|
||||
<span>Subido: {new Date(file.uploadedAt).toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
|
||||
{file.status === 'error' && file.errorMessage && (
|
||||
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-[var(--color-error)]">
|
||||
{file.errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" variant="outline" onClick={() => viewDetails(file.id)}>
|
||||
<Eye className="w-3 h-3" />
|
||||
</Button>
|
||||
|
||||
{file.status === 'error' && (
|
||||
<Button size="sm" variant="outline" onClick={() => retryUpload(file.id)}>
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button size="sm" variant="outline" onClick={() => deleteFile(file.id)}>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Help Section */}
|
||||
<Card className="p-6 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
||||
<h3 className="text-lg font-semibold text-blue-900 mb-4">Tips para la Carga de Datos</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-[var(--color-info)]">
|
||||
<ul className="space-y-2">
|
||||
<li>• Usa las plantillas proporcionadas para garantizar el formato correcto</li>
|
||||
<li>• Verifica que todos los campos requeridos estén completos</li>
|
||||
<li>• Los archivos CSV deben usar codificación UTF-8</li>
|
||||
<li>• Las fechas deben estar en formato DD/MM/YYYY</li>
|
||||
</ul>
|
||||
<ul className="space-y-2">
|
||||
<li>• Los precios deben usar punto (.) como separador decimal</li>
|
||||
<li>• Evita caracteres especiales en los nombres de productos</li>
|
||||
<li>• Mantén los nombres de archivos descriptivos</li>
|
||||
<li>• Puedes cargar múltiples archivos del mismo tipo</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.location.href = '/app/onboarding/setup'}
|
||||
>
|
||||
← Volver a Configuración
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => window.location.href = '/app/onboarding/analysis'}
|
||||
>
|
||||
Continuar al Análisis →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingUploadPage;
|
||||
@@ -1,438 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Upload, File, Check, AlertCircle, Download, RefreshCw, Trash2, Eye } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const OnboardingUploadPage: React.FC = () => {
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const uploadedFiles = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'productos_menu.csv',
|
||||
type: 'productos',
|
||||
size: '45 KB',
|
||||
status: 'completed',
|
||||
uploadedAt: '2024-01-26 10:30:00',
|
||||
records: 127,
|
||||
errors: 3,
|
||||
warnings: 8
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'inventario_inicial.xlsx',
|
||||
type: 'inventario',
|
||||
size: '82 KB',
|
||||
status: 'completed',
|
||||
uploadedAt: '2024-01-26 10:25:00',
|
||||
records: 89,
|
||||
errors: 0,
|
||||
warnings: 2
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'empleados.csv',
|
||||
type: 'empleados',
|
||||
size: '12 KB',
|
||||
status: 'processing',
|
||||
uploadedAt: '2024-01-26 10:35:00',
|
||||
records: 8,
|
||||
errors: 0,
|
||||
warnings: 0
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'ventas_historicas.csv',
|
||||
type: 'ventas',
|
||||
size: '256 KB',
|
||||
status: 'error',
|
||||
uploadedAt: '2024-01-26 10:20:00',
|
||||
records: 0,
|
||||
errors: 1,
|
||||
warnings: 0,
|
||||
errorMessage: 'Formato de fecha incorrecto en la columna "fecha_venta"'
|
||||
}
|
||||
];
|
||||
|
||||
const supportedFormats = [
|
||||
{
|
||||
type: 'productos',
|
||||
name: 'Productos y Menú',
|
||||
formats: ['CSV', 'Excel'],
|
||||
description: 'Lista de productos con precios, categorías y descripciones',
|
||||
template: 'template_productos.csv',
|
||||
requiredFields: ['nombre', 'categoria', 'precio', 'descripcion']
|
||||
},
|
||||
{
|
||||
type: 'inventario',
|
||||
name: 'Inventario Inicial',
|
||||
formats: ['CSV', 'Excel'],
|
||||
description: 'Stock inicial de ingredientes y materias primas',
|
||||
template: 'template_inventario.xlsx',
|
||||
requiredFields: ['item', 'cantidad', 'unidad', 'costo_unitario']
|
||||
},
|
||||
{
|
||||
type: 'empleados',
|
||||
name: 'Empleados',
|
||||
formats: ['CSV'],
|
||||
description: 'Información del personal y roles',
|
||||
template: 'template_empleados.csv',
|
||||
requiredFields: ['nombre', 'cargo', 'email', 'telefono']
|
||||
},
|
||||
{
|
||||
type: 'ventas',
|
||||
name: 'Historial de Ventas',
|
||||
formats: ['CSV'],
|
||||
description: 'Datos históricos de ventas para análisis',
|
||||
template: 'template_ventas.csv',
|
||||
requiredFields: ['fecha', 'producto', 'cantidad', 'total']
|
||||
},
|
||||
{
|
||||
type: 'proveedores',
|
||||
name: 'Proveedores',
|
||||
formats: ['CSV', 'Excel'],
|
||||
description: 'Lista de proveedores y datos de contacto',
|
||||
template: 'template_proveedores.csv',
|
||||
requiredFields: ['nombre', 'contacto', 'telefono', 'email']
|
||||
}
|
||||
];
|
||||
|
||||
const uploadStats = {
|
||||
totalFiles: uploadedFiles.length,
|
||||
completedFiles: uploadedFiles.filter(f => f.status === 'completed').length,
|
||||
totalRecords: uploadedFiles.reduce((sum, f) => sum + f.records, 0),
|
||||
totalErrors: uploadedFiles.reduce((sum, f) => sum + f.errors, 0),
|
||||
totalWarnings: uploadedFiles.reduce((sum, f) => sum + f.warnings, 0)
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
const iconProps = { className: "w-5 h-5" };
|
||||
switch (status) {
|
||||
case 'completed': return <Check {...iconProps} className="w-5 h-5 text-green-600" />;
|
||||
case 'processing': return <RefreshCw {...iconProps} className="w-5 h-5 text-blue-600 animate-spin" />;
|
||||
case 'error': return <AlertCircle {...iconProps} className="w-5 h-5 text-red-600" />;
|
||||
default: return <File {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'green';
|
||||
case 'processing': return 'blue';
|
||||
case 'error': return 'red';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'Completado';
|
||||
case 'processing': return 'Procesando';
|
||||
case 'error': return 'Error';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragActive(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragActive(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragActive(false);
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
handleFiles(files);
|
||||
};
|
||||
|
||||
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
const files = Array.from(e.target.files);
|
||||
handleFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFiles = (files: File[]) => {
|
||||
console.log('Files selected:', files);
|
||||
// Simulate upload progress
|
||||
setIsProcessing(true);
|
||||
setUploadProgress(0);
|
||||
const interval = setInterval(() => {
|
||||
setUploadProgress(prev => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(interval);
|
||||
setIsProcessing(false);
|
||||
return 100;
|
||||
}
|
||||
return prev + 10;
|
||||
});
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const downloadTemplate = (template: string) => {
|
||||
console.log('Downloading template:', template);
|
||||
// Handle template download
|
||||
};
|
||||
|
||||
const retryUpload = (fileId: string) => {
|
||||
console.log('Retrying upload for file:', fileId);
|
||||
// Handle retry logic
|
||||
};
|
||||
|
||||
const deleteFile = (fileId: string) => {
|
||||
console.log('Deleting file:', fileId);
|
||||
// Handle delete logic
|
||||
};
|
||||
|
||||
const viewDetails = (fileId: string) => {
|
||||
console.log('Viewing details for file:', fileId);
|
||||
// Handle view details logic
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Carga de Datos"
|
||||
description="Importa tus datos existentes para acelerar la configuración inicial"
|
||||
/>
|
||||
|
||||
{/* Upload Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Archivos Subidos</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{uploadStats.totalFiles}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<Upload className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Completados</p>
|
||||
<p className="text-3xl font-bold text-green-600">{uploadStats.completedFiles}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Check className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Registros</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{uploadStats.totalRecords}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<File className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Errores</p>
|
||||
<p className="text-3xl font-bold text-red-600">{uploadStats.totalErrors}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
|
||||
<AlertCircle className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Advertencias</p>
|
||||
<p className="text-3xl font-bold text-yellow-600">{uploadStats.totalWarnings}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||
<AlertCircle className="h-6 w-6 text-yellow-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Upload Area */}
|
||||
<Card className="p-8">
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
||||
dragActive
|
||||
? 'border-blue-400 bg-blue-50'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
Arrastra archivos aquí o haz clic para seleccionar
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Formatos soportados: CSV, Excel (XLSX). Tamaño máximo: 10MB por archivo
|
||||
</p>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
accept=".csv,.xlsx,.xls"
|
||||
onChange={handleFileInput}
|
||||
className="hidden"
|
||||
id="file-upload"
|
||||
/>
|
||||
<label htmlFor="file-upload">
|
||||
<Button className="cursor-pointer">
|
||||
Seleccionar Archivos
|
||||
</Button>
|
||||
</label>
|
||||
|
||||
{isProcessing && (
|
||||
<div className="mt-4">
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 mb-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">Procesando... {uploadProgress}%</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Supported Formats */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Formatos Soportados</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{supportedFormats.map((format, index) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-gray-900">{format.name}</h4>
|
||||
<div className="flex space-x-1">
|
||||
{format.formats.map((fmt, idx) => (
|
||||
<Badge key={idx} variant="blue" className="text-xs">{fmt}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 mb-3">{format.description}</p>
|
||||
|
||||
<div className="mb-3">
|
||||
<p className="text-xs font-medium text-gray-700 mb-1">Campos requeridos:</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{format.requiredFields.map((field, idx) => (
|
||||
<span key={idx} className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded">
|
||||
{field}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => downloadTemplate(format.template)}
|
||||
>
|
||||
<Download className="w-3 h-3 mr-2" />
|
||||
Descargar Plantilla
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Uploaded Files */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Archivos Cargados</h3>
|
||||
<div className="space-y-3">
|
||||
{uploadedFiles.map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-center space-x-4 flex-1">
|
||||
{getStatusIcon(file.status)}
|
||||
|
||||
<div>
|
||||
<div className="flex items-center space-x-3 mb-1">
|
||||
<h4 className="text-sm font-medium text-gray-900">{file.name}</h4>
|
||||
<Badge variant={getStatusColor(file.status)}>
|
||||
{getStatusLabel(file.status)}
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-500">{file.size}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4 text-xs text-gray-600">
|
||||
<span>{file.records} registros</span>
|
||||
{file.errors > 0 && (
|
||||
<span className="text-red-600">{file.errors} errores</span>
|
||||
)}
|
||||
{file.warnings > 0 && (
|
||||
<span className="text-yellow-600">{file.warnings} advertencias</span>
|
||||
)}
|
||||
<span>Subido: {new Date(file.uploadedAt).toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
|
||||
{file.status === 'error' && file.errorMessage && (
|
||||
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700">
|
||||
{file.errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" variant="outline" onClick={() => viewDetails(file.id)}>
|
||||
<Eye className="w-3 h-3" />
|
||||
</Button>
|
||||
|
||||
{file.status === 'error' && (
|
||||
<Button size="sm" variant="outline" onClick={() => retryUpload(file.id)}>
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button size="sm" variant="outline" onClick={() => deleteFile(file.id)}>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Help Section */}
|
||||
<Card className="p-6 bg-blue-50 border-blue-200">
|
||||
<h3 className="text-lg font-semibold text-blue-900 mb-4">Tips para la Carga de Datos</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-blue-800">
|
||||
<ul className="space-y-2">
|
||||
<li>• Usa las plantillas proporcionadas para garantizar el formato correcto</li>
|
||||
<li>• Verifica que todos los campos requeridos estén completos</li>
|
||||
<li>• Los archivos CSV deben usar codificación UTF-8</li>
|
||||
<li>• Las fechas deben estar en formato DD/MM/YYYY</li>
|
||||
</ul>
|
||||
<ul className="space-y-2">
|
||||
<li>• Los precios deben usar punto (.) como separador decimal</li>
|
||||
<li>• Evita caracteres especiales en los nombres de productos</li>
|
||||
<li>• Mantén los nombres de archivos descriptivos</li>
|
||||
<li>• Puedes cargar múltiples archivos del mismo tipo</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingUploadPage;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as OnboardingUploadPage } from './OnboardingUploadPage';
|
||||
@@ -1,710 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '../../components/ui';
|
||||
import { PublicLayout } from '../../components/layout';
|
||||
import {
|
||||
BarChart3,
|
||||
TrendingUp,
|
||||
Shield,
|
||||
Zap,
|
||||
Users,
|
||||
Award,
|
||||
ChevronRight,
|
||||
Check,
|
||||
Star,
|
||||
ArrowRight,
|
||||
Play,
|
||||
Calendar,
|
||||
Clock,
|
||||
DollarSign,
|
||||
Package,
|
||||
PieChart,
|
||||
Settings
|
||||
} from 'lucide-react';
|
||||
|
||||
const LandingPage: React.FC = () => {
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PublicLayout
|
||||
variant="full-width"
|
||||
contentPadding="none"
|
||||
headerProps={{
|
||||
showThemeToggle: true,
|
||||
showAuthButtons: true,
|
||||
variant: "default",
|
||||
navigationItems: [
|
||||
{ id: 'features', label: 'Características', href: '#features' },
|
||||
{ id: 'benefits', label: 'Beneficios', href: '#benefits' },
|
||||
{ id: 'pricing', label: 'Precios', href: '#pricing' },
|
||||
{ id: 'testimonials', label: 'Testimonios', href: '#testimonials' }
|
||||
]
|
||||
}}
|
||||
>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative py-20 lg:py-32 bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<div className="mb-6">
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
IA Avanzada para Panaderías
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl tracking-tight font-extrabold text-[var(--text-primary)] sm:text-5xl lg:text-7xl">
|
||||
<span className="block">Revoluciona tu</span>
|
||||
<span className="block text-[var(--color-primary)]">Panadería con IA</span>
|
||||
</h1>
|
||||
|
||||
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)] sm:text-xl">
|
||||
Optimiza automáticamente tu producción, reduce desperdicios hasta un 35%,
|
||||
predice demanda con precisión del 92% y aumenta tus ventas con inteligencia artificial.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link to="/register">
|
||||
<Button size="lg" className="px-8 py-4 text-lg font-semibold bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200">
|
||||
Comenzar Gratis 14 Días
|
||||
<ArrowRight className="ml-2 w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="px-8 py-4 text-lg font-semibold border-2 border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-white transition-all duration-200"
|
||||
onClick={() => scrollToSection('demo')}
|
||||
>
|
||||
<Play className="mr-2 w-5 h-5" />
|
||||
Ver Demo en Vivo
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 flex items-center justify-center space-x-6 text-sm text-[var(--text-tertiary)]">
|
||||
<div className="flex items-center">
|
||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
||||
Sin tarjeta de crédito
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
||||
Configuración en 5 minutos
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
||||
Soporte 24/7 en español
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background decoration */}
|
||||
<div className="absolute top-0 left-0 right-0 h-full overflow-hidden -z-10">
|
||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-[var(--color-primary)]/5 rounded-full blur-3xl"></div>
|
||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-[var(--color-secondary)]/5 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats Section */}
|
||||
<section className="py-16 bg-[var(--bg-primary)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-[var(--color-primary)]">500+</div>
|
||||
<div className="mt-2 text-sm text-[var(--text-secondary)]">Panaderías Activas</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-[var(--color-primary)]">35%</div>
|
||||
<div className="mt-2 text-sm text-[var(--text-secondary)]">Reducción de Desperdicios</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-[var(--color-primary)]">92%</div>
|
||||
<div className="mt-2 text-sm text-[var(--text-secondary)]">Precisión de Predicciones</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-[var(--color-primary)]">4.8★</div>
|
||||
<div className="mt-2 text-sm text-[var(--text-secondary)]">Satisfacción de Clientes</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Features Section */}
|
||||
<section id="features" className="py-24 bg-[var(--bg-secondary)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl lg:text-5xl font-extrabold text-[var(--text-primary)]">
|
||||
Gestión Completa con
|
||||
<span className="block text-[var(--color-primary)]">Inteligencia Artificial</span>
|
||||
</h2>
|
||||
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)]">
|
||||
Automatiza procesos, optimiza recursos y toma decisiones inteligentes basadas en datos reales de tu panadería.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-20 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* AI Forecasting */}
|
||||
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
|
||||
<div className="absolute -top-4 left-8">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-xl flex items-center justify-center shadow-lg">
|
||||
<TrendingUp className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)]">Predicción Inteligente</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
Algoritmos de IA analizan patrones históricos, clima, eventos locales y tendencias para predecir la demanda exacta de cada producto.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center text-sm text-[var(--color-primary)]">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Precisión del 92% en predicciones
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-[var(--color-primary)] mt-2">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Reduce desperdicios hasta 35%
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-[var(--color-primary)] mt-2">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Aumenta ventas promedio 22%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Smart Inventory */}
|
||||
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
|
||||
<div className="absolute -top-4 left-8">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-secondary)] to-[var(--color-secondary-dark)] rounded-xl flex items-center justify-center shadow-lg">
|
||||
<Package className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)]">Inventario Inteligente</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
Control automático de stock con alertas predictivas, órdenes de compra automatizadas y optimización de costos.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center text-sm text-[var(--color-secondary)]">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Alertas automáticas de stock bajo
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-[var(--color-secondary)] mt-2">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Órdenes de compra automatizadas
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-[var(--color-secondary)] mt-2">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Optimización de costos de materias primas
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Production Planning */}
|
||||
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
|
||||
<div className="absolute -top-4 left-8">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-accent)] to-[var(--color-accent-dark)] rounded-xl flex items-center justify-center shadow-lg">
|
||||
<Calendar className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)]">Planificación de Producción</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
Programa automáticamente la producción diaria basada en predicciones, optimiza horarios y recursos disponibles.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center text-sm text-[var(--color-accent)]">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Programación automática de horneado
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-[var(--color-accent)] mt-2">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Optimización de uso de hornos
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-[var(--color-accent)] mt-2">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Gestión de personal y turnos
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Features Grid */}
|
||||
<div className="mt-16 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
|
||||
<div className="w-12 h-12 bg-[var(--color-primary)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<BarChart3 className="w-6 h-6 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)]">Analytics Avanzado</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-2">Dashboards en tiempo real con métricas clave</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
|
||||
<div className="w-12 h-12 bg-[var(--color-secondary)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<DollarSign className="w-6 h-6 text-[var(--color-secondary)]" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)]">POS Integrado</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-2">Sistema de ventas completo y fácil de usar</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
|
||||
<div className="w-12 h-12 bg-[var(--color-accent)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<Shield className="w-6 h-6 text-[var(--color-accent)]" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)]">Control de Calidad</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-2">Trazabilidad completa y gestión HACCP</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
|
||||
<div className="w-12 h-12 bg-[var(--color-info)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<Settings className="w-6 h-6 text-[var(--color-info)]" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)]">Automatización</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-2">Procesos automáticos que ahorran tiempo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Benefits Section */}
|
||||
<section id="benefits" className="py-24 bg-[var(--bg-primary)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="lg:grid lg:grid-cols-2 lg:gap-16 items-center">
|
||||
<div>
|
||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||
Resultados Comprobados
|
||||
<span className="block text-[var(--color-primary)]">en Cientos de Panaderías</span>
|
||||
</h2>
|
||||
<p className="mt-6 text-lg text-[var(--text-secondary)]">
|
||||
Nuestros clientes han logrado transformaciones significativas en sus operaciones,
|
||||
mejorando rentabilidad y reduciendo desperdicios desde el primer mes.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 space-y-8">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<TrendingUp className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Aumenta Ventas 22% Promedio</h4>
|
||||
<p className="text-[var(--text-secondary)]">Optimización de producción y mejor disponibilidad de productos populares</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<Shield className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Reduce Desperdicios 35%</h4>
|
||||
<p className="text-[var(--text-secondary)]">Predicciones precisas evitan sobreproducción y productos vencidos</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<Clock className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Ahorra 8 Horas Semanales</h4>
|
||||
<p className="text-[var(--text-secondary)]">Automatización de tareas administrativas y de planificación</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 lg:mt-0">
|
||||
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-secondary)]/10 rounded-2xl p-8">
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-[var(--color-primary)]">€127k</div>
|
||||
<div className="text-sm text-[var(--text-secondary)]">Ahorro promedio anual por panadería</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-[var(--color-secondary)]">98%</div>
|
||||
<div className="text-sm text-[var(--text-secondary)]">Satisfacción de clientes</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-[var(--color-accent)]">2.3x</div>
|
||||
<div className="text-sm text-[var(--text-secondary)]">ROI promedio en 12 meses</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-[var(--color-info)]">24/7</div>
|
||||
<div className="text-sm text-[var(--text-secondary)]">Soporte técnico especializado</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Testimonials Section */}
|
||||
<section id="testimonials" className="py-24 bg-[var(--bg-secondary)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||
Lo que Dicen Nuestros Clientes
|
||||
</h2>
|
||||
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
|
||||
Panaderías de toda España han transformado sus negocios con nuestra plataforma
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Testimonial 1 */}
|
||||
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
|
||||
<div className="flex items-center mb-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
|
||||
))}
|
||||
</div>
|
||||
<blockquote className="text-[var(--text-secondary)] italic">
|
||||
"Desde que implementamos Panadería IA, nuestros desperdicios se redujeron un 40% y las ventas aumentaron un 28%.
|
||||
La predicción de demanda es increíblemente precisa."
|
||||
</blockquote>
|
||||
<div className="mt-6 flex items-center">
|
||||
<div className="w-12 h-12 bg-[var(--color-primary)] rounded-full flex items-center justify-center text-white font-bold">
|
||||
M
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-[var(--text-primary)]">María González</div>
|
||||
<div className="text-sm text-[var(--text-secondary)]">Panadería Santa María, Madrid</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Testimonial 2 */}
|
||||
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
|
||||
<div className="flex items-center mb-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
|
||||
))}
|
||||
</div>
|
||||
<blockquote className="text-[var(--text-secondary)] italic">
|
||||
"El sistema nos ahorra 10 horas semanales en planificación. Ahora puedo enfocarme en mejorar nuestros productos
|
||||
mientras la IA maneja la logística."
|
||||
</blockquote>
|
||||
<div className="mt-6 flex items-center">
|
||||
<div className="w-12 h-12 bg-[var(--color-secondary)] rounded-full flex items-center justify-center text-white font-bold">
|
||||
C
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-[var(--text-primary)]">Carlos Ruiz</div>
|
||||
<div className="text-sm text-[var(--text-secondary)]">Horno de Oro, Valencia</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Testimonial 3 */}
|
||||
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
|
||||
<div className="flex items-center mb-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
|
||||
))}
|
||||
</div>
|
||||
<blockquote className="text-[var(--text-secondary)] italic">
|
||||
"Increíble cómo predice exactamente cuántos panes necesitamos cada día. Nuestros clientes siempre encuentran
|
||||
sus productos favoritos disponibles."
|
||||
</blockquote>
|
||||
<div className="mt-6 flex items-center">
|
||||
<div className="w-12 h-12 bg-[var(--color-accent)] rounded-full flex items-center justify-center text-white font-bold">
|
||||
A
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-[var(--text-primary)]">Ana Martínez</div>
|
||||
<div className="text-sm text-[var(--text-secondary)]">Pan & Tradición, Sevilla</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trust indicators */}
|
||||
<div className="mt-16 text-center">
|
||||
<p className="text-sm text-[var(--text-tertiary)] mb-8">Confiado por más de 500 panaderías en España</p>
|
||||
<div className="flex items-center justify-center space-x-8 opacity-60">
|
||||
<div className="font-semibold text-[var(--text-secondary)]">Panadería Real</div>
|
||||
<div className="font-semibold text-[var(--text-secondary)]">Horno Artesanal</div>
|
||||
<div className="font-semibold text-[var(--text-secondary)]">Pan de Casa</div>
|
||||
<div className="font-semibold text-[var(--text-secondary)]">Dulce Tradición</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Pricing Section */}
|
||||
<section id="pricing" className="py-24 bg-[var(--bg-primary)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||
Planes que se Adaptan a tu Negocio
|
||||
</h2>
|
||||
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
|
||||
Sin costos ocultos, sin compromisos largos. Comienza gratis y escala según crezcas.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Starter Plan */}
|
||||
<div className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Starter</h3>
|
||||
<p className="mt-2 text-sm text-[var(--text-secondary)]">Perfecto para panaderías pequeñas</p>
|
||||
<div className="mt-6">
|
||||
<span className="text-3xl font-bold text-[var(--text-primary)]">€49</span>
|
||||
<span className="text-[var(--text-secondary)]">/mes</span>
|
||||
</div>
|
||||
<div className="mt-8 space-y-4">
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">Hasta 50 productos</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">Predicción básica de demanda</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">Control de inventario</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">Reportes básicos</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">Soporte por email</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full mt-8" variant="outline">
|
||||
Comenzar Gratis
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Professional Plan - Highlighted */}
|
||||
<div className="bg-gradient-to-b from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-2xl p-8 relative shadow-2xl">
|
||||
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
||||
<span className="bg-[var(--color-secondary)] text-white px-4 py-1 rounded-full text-sm font-semibold">
|
||||
Más Popular
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white">Professional</h3>
|
||||
<p className="mt-2 text-sm text-white/80">Para panaderías en crecimiento</p>
|
||||
<div className="mt-6">
|
||||
<span className="text-3xl font-bold text-white">€149</span>
|
||||
<span className="text-white/80">/mes</span>
|
||||
</div>
|
||||
<div className="mt-8 space-y-4">
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-white mr-3" />
|
||||
<span className="text-sm text-white">Productos ilimitados</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-white mr-3" />
|
||||
<span className="text-sm text-white">IA avanzada con 92% precisión</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-white mr-3" />
|
||||
<span className="text-sm text-white">Gestión completa de producción</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-white mr-3" />
|
||||
<span className="text-sm text-white">POS integrado</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-white mr-3" />
|
||||
<span className="text-sm text-white">Analytics avanzado</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-white mr-3" />
|
||||
<span className="text-sm text-white">Soporte prioritario 24/7</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full mt-8 bg-white text-[var(--color-primary)] hover:bg-gray-100">
|
||||
Comenzar Prueba Gratuita
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Enterprise Plan */}
|
||||
<div className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Enterprise</h3>
|
||||
<p className="mt-2 text-sm text-[var(--text-secondary)]">Para cadenas y grandes operaciones</p>
|
||||
<div className="mt-6">
|
||||
<span className="text-3xl font-bold text-[var(--text-primary)]">€399</span>
|
||||
<span className="text-[var(--text-secondary)]">/mes</span>
|
||||
</div>
|
||||
<div className="mt-8 space-y-4">
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">Multi-locación ilimitada</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">IA personalizada por ubicación</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">API personalizada</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">Integración ERPs</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">Manager dedicado</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full mt-8" variant="outline">
|
||||
Contactar Ventas
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 text-center">
|
||||
<p className="text-sm text-[var(--text-tertiary)]">
|
||||
🔒 Todos los planes incluyen cifrado de datos, backups automáticos y cumplimiento RGPD
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="py-24 bg-[var(--bg-secondary)]">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||
Preguntas Frecuentes
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-[var(--text-secondary)]">
|
||||
Todo lo que necesitas saber sobre Panadería IA
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 space-y-8">
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
¿Qué tan precisa es la predicción de demanda?
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
Nuestra IA alcanza una precisión del 92% en predicciones de demanda, analizando más de 50 variables incluyendo
|
||||
histórico de ventas, clima, eventos locales, estacionalidad y tendencias de mercado. La precisión mejora continuamente
|
||||
con más datos de tu panadería.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
¿Cuánto tiempo toma implementar el sistema?
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
La configuración inicial toma solo 5 minutos. Nuestro equipo te ayuda a migrar tus datos históricos en 24-48 horas.
|
||||
La IA comienza a generar predicciones útiles después de una semana de datos, alcanzando máxima precisión en 30 días.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
¿Se integra con mi sistema POS actual?
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
Sí, nos integramos con más de 50 sistemas POS populares en España. También incluimos nuestro propio POS optimizado
|
||||
para panaderías. Si usas un sistema específico, nuestro equipo técnico puede crear una integración personalizada.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
¿Qué soporte técnico ofrecen?
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
Ofrecemos soporte 24/7 en español por chat, email y teléfono. Todos nuestros técnicos son expertos en operaciones
|
||||
de panadería. Además, incluimos onboarding personalizado y training para tu equipo sin costo adicional.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
¿Mis datos están seguros?
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
Absolutamente. Utilizamos cifrado AES-256, servidores en la UE, cumplimos 100% con RGPD y realizamos auditorías
|
||||
de seguridad trimestrales. Tus datos nunca se comparten con terceros y tienes control total sobre tu información.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Final CTA Section */}
|
||||
<section className="py-24 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] relative overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8 relative">
|
||||
<h2 className="text-3xl lg:text-5xl font-extrabold text-white">
|
||||
Transforma tu Panadería
|
||||
<span className="block text-white/90">Comenzando Hoy</span>
|
||||
</h2>
|
||||
<p className="mt-6 text-lg text-white/80 max-w-2xl mx-auto">
|
||||
Únete a más de 500 panaderías que ya están reduciendo desperdicios, aumentando ventas y
|
||||
optimizando operaciones con inteligencia artificial.
|
||||
</p>
|
||||
|
||||
<div className="mt-12 flex flex-col sm:flex-row gap-6 justify-center">
|
||||
<Link to="/register">
|
||||
<Button
|
||||
size="lg"
|
||||
className="px-10 py-4 text-lg font-semibold bg-white text-[var(--color-primary)] hover:bg-gray-100 shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200"
|
||||
>
|
||||
Comenzar Prueba Gratuita 14 Días
|
||||
<ArrowRight className="ml-2 w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="px-10 py-4 text-lg font-semibold border-2 border-white text-white hover:bg-white hover:text-[var(--color-primary)] transition-all duration-200"
|
||||
onClick={() => scrollToSection('demo')}
|
||||
>
|
||||
<Play className="mr-2 w-5 h-5" />
|
||||
Ver Demo
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 grid grid-cols-1 sm:grid-cols-3 gap-8 text-center">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">14 días</div>
|
||||
<div className="text-white/70 text-sm">Prueba gratuita</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">5 min</div>
|
||||
<div className="text-white/70 text-sm">Configuración</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">24/7</div>
|
||||
<div className="text-white/70 text-sm">Soporte incluido</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default LandingPage;
|
||||
@@ -7,7 +7,7 @@ const RegisterPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleRegistrationSuccess = () => {
|
||||
navigate('/app/onboarding/setup');
|
||||
navigate('/app/onboarding');
|
||||
};
|
||||
|
||||
const handleLoginClick = () => {
|
||||
|
||||
@@ -39,10 +39,7 @@ const TrafficPage = React.lazy(() => import('../pages/app/data/traffic/TrafficPa
|
||||
const EventsPage = React.lazy(() => import('../pages/app/data/events/EventsPage'));
|
||||
|
||||
// Onboarding pages
|
||||
const OnboardingSetupPage = React.lazy(() => import('../pages/app/onboarding/setup/OnboardingSetupPage'));
|
||||
const OnboardingUploadPage = React.lazy(() => import('../pages/app/onboarding/upload/OnboardingUploadPage'));
|
||||
const OnboardingAnalysisPage = React.lazy(() => import('../pages/app/onboarding/analysis/OnboardingAnalysisPage'));
|
||||
const OnboardingReviewPage = React.lazy(() => import('../pages/app/onboarding/review/OnboardingReviewPage'));
|
||||
const OnboardingPage = React.lazy(() => import('../pages/app/onboarding/OnboardingPage'));
|
||||
|
||||
export const AppRouter: React.FC = () => {
|
||||
return (
|
||||
@@ -277,44 +274,12 @@ export const AppRouter: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Onboarding Routes */}
|
||||
{/* Onboarding Route - New Complete Flow */}
|
||||
<Route
|
||||
path="/app/onboarding/setup"
|
||||
path="/app/onboarding"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<OnboardingSetupPage />
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/app/onboarding/upload"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<OnboardingUploadPage />
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/app/onboarding/analysis"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<OnboardingAnalysisPage />
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/app/onboarding/review"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<OnboardingReviewPage />
|
||||
</AppShell>
|
||||
<OnboardingPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -468,57 +468,21 @@ export const routesConfig: RouteConfig[] = [
|
||||
],
|
||||
},
|
||||
|
||||
// Onboarding Section - Hidden from navigation
|
||||
// Onboarding Section - Complete 9-step flow
|
||||
{
|
||||
path: '/app/onboarding',
|
||||
name: 'Onboarding',
|
||||
component: 'OnboardingPage',
|
||||
title: 'Configuración Inicial',
|
||||
description: 'Configuración completa en 9 pasos con IA',
|
||||
icon: 'settings',
|
||||
requiresAuth: true,
|
||||
showInNavigation: false,
|
||||
children: [
|
||||
{
|
||||
path: '/app/onboarding/setup',
|
||||
name: 'OnboardingSetup',
|
||||
component: 'OnboardingSetupPage',
|
||||
title: 'Configuración Básica',
|
||||
icon: 'settings',
|
||||
requiresAuth: true,
|
||||
showInNavigation: false,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
{
|
||||
path: '/app/onboarding/upload',
|
||||
name: 'OnboardingUpload',
|
||||
component: 'OnboardingUploadPage',
|
||||
title: 'Carga de Datos',
|
||||
icon: 'data',
|
||||
requiresAuth: true,
|
||||
showInNavigation: false,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
{
|
||||
path: '/app/onboarding/analysis',
|
||||
name: 'OnboardingAnalysis',
|
||||
component: 'OnboardingAnalysisPage',
|
||||
title: 'Análisis de Datos',
|
||||
icon: 'forecasting',
|
||||
requiresAuth: true,
|
||||
showInNavigation: false,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
{
|
||||
path: '/app/onboarding/review',
|
||||
name: 'OnboardingReview',
|
||||
component: 'OnboardingReviewPage',
|
||||
title: 'Revisión Final',
|
||||
icon: 'settings',
|
||||
requiresAuth: true,
|
||||
showInNavigation: false,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
hideHeader: true,
|
||||
hideSidebar: true,
|
||||
fullScreen: true,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user