Add onboarding flow improvements

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

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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';

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View 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 (&lt;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>
);
};

View File

@@ -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>
);
};

View 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;
};

View 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');
}
};
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;

View File

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

View File

@@ -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>
}
/>

View File

@@ -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,
},
},