Add onboarding flow improvements
This commit is contained in:
@@ -101,7 +101,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error calling onSuccess:', error);
|
console.error('Error calling onSuccess:', error);
|
||||||
// Fallback: direct redirect if callback fails
|
// Fallback: direct redirect if callback fails
|
||||||
window.location.href = '/app/onboarding/setup';
|
window.location.href = '/app/onboarding';
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,402 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Input, Select, Badge } from '../../ui';
|
|
||||||
import type { OnboardingStepProps } from './OnboardingWizard';
|
|
||||||
|
|
||||||
interface CompanyInfo {
|
|
||||||
name: string;
|
|
||||||
type: 'artisan' | 'dependent';
|
|
||||||
size: 'small' | 'medium' | 'large';
|
|
||||||
locations: number;
|
|
||||||
specialties: string[];
|
|
||||||
address: {
|
|
||||||
street: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postal_code: string;
|
|
||||||
country: string;
|
|
||||||
};
|
|
||||||
contact: {
|
|
||||||
phone: string;
|
|
||||||
email: string;
|
|
||||||
website?: string;
|
|
||||||
};
|
|
||||||
established_year?: number;
|
|
||||||
tax_id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BAKERY_TYPES = [
|
|
||||||
{ value: 'artisan', label: 'Panadería Artesanal Local', description: 'Producción propia y tradicional en el local' },
|
|
||||||
{ value: 'dependent', label: 'Panadería Dependiente', description: 'Dependiente de un panadero central' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const BAKERY_SIZES = [
|
|
||||||
{ value: 'small', label: 'Pequeña', description: '1-10 empleados' },
|
|
||||||
{ value: 'medium', label: 'Mediana', description: '11-50 empleados' },
|
|
||||||
{ value: 'large', label: 'Grande', description: '50+ empleados' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const COMMON_SPECIALTIES = [
|
|
||||||
'Pan tradicional',
|
|
||||||
'Bollería',
|
|
||||||
'Repostería',
|
|
||||||
'Pan integral',
|
|
||||||
'Pasteles',
|
|
||||||
'Productos sin gluten',
|
|
||||||
'Productos veganos',
|
|
||||||
'Pan artesanal',
|
|
||||||
'Productos de temporada',
|
|
||||||
'Catering',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const CompanyInfoStep: React.FC<OnboardingStepProps> = ({
|
|
||||||
data,
|
|
||||||
onDataChange,
|
|
||||||
}) => {
|
|
||||||
const companyData: CompanyInfo = {
|
|
||||||
name: '',
|
|
||||||
type: 'artisan',
|
|
||||||
size: 'small',
|
|
||||||
locations: 1,
|
|
||||||
specialties: [],
|
|
||||||
address: {
|
|
||||||
street: '',
|
|
||||||
city: '',
|
|
||||||
state: '',
|
|
||||||
postal_code: '',
|
|
||||||
country: 'España',
|
|
||||||
},
|
|
||||||
contact: {
|
|
||||||
phone: '',
|
|
||||||
email: '',
|
|
||||||
website: '',
|
|
||||||
},
|
|
||||||
...data,
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: any) => {
|
|
||||||
onDataChange({
|
|
||||||
...companyData,
|
|
||||||
[field]: value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddressChange = (field: string, value: string) => {
|
|
||||||
onDataChange({
|
|
||||||
...companyData,
|
|
||||||
address: {
|
|
||||||
...companyData.address,
|
|
||||||
[field]: value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContactChange = (field: string, value: string) => {
|
|
||||||
onDataChange({
|
|
||||||
...companyData,
|
|
||||||
contact: {
|
|
||||||
...companyData.contact,
|
|
||||||
[field]: value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSpecialtyToggle = (specialty: string) => {
|
|
||||||
const currentSpecialties = companyData.specialties || [];
|
|
||||||
const updatedSpecialties = currentSpecialties.includes(specialty)
|
|
||||||
? currentSpecialties.filter(s => s !== specialty)
|
|
||||||
: [...currentSpecialties, specialty];
|
|
||||||
|
|
||||||
handleInputChange('specialties', updatedSpecialties);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-8">
|
|
||||||
{/* Basic Information */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
||||||
Información básica
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Nombre de la panadería *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={companyData.name}
|
|
||||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
|
||||||
placeholder="Ej: Panadería San Miguel"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Tipo de panadería *
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={companyData.type}
|
|
||||||
onChange={(e) => handleInputChange('type', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{BAKERY_TYPES.map((type) => (
|
|
||||||
<option key={type.value} value={type.value}>
|
|
||||||
{type.label} - {type.description}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Tamaño de la empresa *
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={companyData.size}
|
|
||||||
onChange={(e) => handleInputChange('size', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{BAKERY_SIZES.map((size) => (
|
|
||||||
<option key={size.value} value={size.value}>
|
|
||||||
{size.label} - {size.description}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Número de ubicaciones
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value={companyData.locations}
|
|
||||||
onChange={(e) => handleInputChange('locations', parseInt(e.target.value) || 1)}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Año de fundación
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min="1900"
|
|
||||||
max={new Date().getFullYear()}
|
|
||||||
value={companyData.established_year || ''}
|
|
||||||
onChange={(e) => handleInputChange('established_year', parseInt(e.target.value) || undefined)}
|
|
||||||
placeholder="Ej: 1995"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
NIF/CIF
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={companyData.tax_id || ''}
|
|
||||||
onChange={(e) => handleInputChange('tax_id', e.target.value)}
|
|
||||||
placeholder="Ej: B12345678"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Specialties */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
||||||
Especialidades
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)]">
|
|
||||||
Selecciona los productos que produces habitualmente
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
||||||
{COMMON_SPECIALTIES.map((specialty) => (
|
|
||||||
<button
|
|
||||||
key={specialty}
|
|
||||||
onClick={() => handleSpecialtyToggle(specialty)}
|
|
||||||
className={`p-3 text-left border rounded-lg transition-colors ${
|
|
||||||
companyData.specialties?.includes(specialty)
|
|
||||||
? 'border-blue-500 bg-[var(--color-info)]/5 text-[var(--color-info)]'
|
|
||||||
: 'border-[var(--border-primary)] hover:border-[var(--border-secondary)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="text-sm font-medium">{specialty}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{companyData.specialties && companyData.specialties.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Especialidades seleccionadas:
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{companyData.specialties.map((specialty) => (
|
|
||||||
<Badge key={specialty} className="bg-[var(--color-info)]/10 text-[var(--color-info)]">
|
|
||||||
{specialty}
|
|
||||||
<button
|
|
||||||
onClick={() => handleSpecialtyToggle(specialty)}
|
|
||||||
className="ml-1 text-[var(--color-info)] hover:text-[var(--color-info)]"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Address */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
||||||
Dirección principal
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Dirección *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={companyData.address.street}
|
|
||||||
onChange={(e) => handleAddressChange('street', e.target.value)}
|
|
||||||
placeholder="Calle, número, piso, puerta"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Ciudad *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={companyData.address.city}
|
|
||||||
onChange={(e) => handleAddressChange('city', e.target.value)}
|
|
||||||
placeholder="Ej: Madrid"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Provincia/Estado *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={companyData.address.state}
|
|
||||||
onChange={(e) => handleAddressChange('state', e.target.value)}
|
|
||||||
placeholder="Ej: Madrid"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Código postal *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={companyData.address.postal_code}
|
|
||||||
onChange={(e) => handleAddressChange('postal_code', e.target.value)}
|
|
||||||
placeholder="Ej: 28001"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
País *
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={companyData.address.country}
|
|
||||||
onChange={(e) => handleAddressChange('country', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<option value="España">España</option>
|
|
||||||
<option value="Francia">Francia</option>
|
|
||||||
<option value="Portugal">Portugal</option>
|
|
||||||
<option value="Italia">Italia</option>
|
|
||||||
<option value="México">México</option>
|
|
||||||
<option value="Argentina">Argentina</option>
|
|
||||||
<option value="Colombia">Colombia</option>
|
|
||||||
<option value="Otro">Otro</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contact Information */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
||||||
Información de contacto
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Teléfono *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="tel"
|
|
||||||
value={companyData.contact.phone}
|
|
||||||
onChange={(e) => handleContactChange('phone', e.target.value)}
|
|
||||||
placeholder="Ej: +34 911 234 567"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Email de contacto *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
value={companyData.contact.email}
|
|
||||||
onChange={(e) => handleContactChange('email', e.target.value)}
|
|
||||||
placeholder="contacto@panaderia.com"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Sitio web (opcional)
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="url"
|
|
||||||
value={companyData.contact.website || ''}
|
|
||||||
onChange={(e) => handleContactChange('website', e.target.value)}
|
|
||||||
placeholder="https://www.panaderia.com"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Summary */}
|
|
||||||
<div className="bg-[var(--bg-secondary)] p-4 rounded-lg">
|
|
||||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Resumen</h4>
|
|
||||||
<div className="text-sm text-[var(--text-secondary)] space-y-1">
|
|
||||||
<p><strong>Panadería:</strong> {companyData.name || 'Sin especificar'}</p>
|
|
||||||
<p><strong>Tipo:</strong> {BAKERY_TYPES.find(t => t.value === companyData.type)?.label}</p>
|
|
||||||
<p><strong>Tamaño:</strong> {BAKERY_SIZES.find(s => s.value === companyData.size)?.label}</p>
|
|
||||||
<p><strong>Especialidades:</strong> {companyData.specialties?.length || 0} seleccionadas</p>
|
|
||||||
<p><strong>Ubicaciones:</strong> {companyData.locations}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CompanyInfoStep;
|
|
||||||
@@ -74,7 +74,67 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
|
|||||||
return true;
|
return true;
|
||||||
}, [currentStep, stepData]);
|
}, [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 (validateCurrentStep()) {
|
||||||
if (currentStepIndex < steps.length - 1) {
|
if (currentStepIndex < steps.length - 1) {
|
||||||
setCurrentStepIndex(currentStepIndex + 1);
|
setCurrentStepIndex(currentStepIndex + 1);
|
||||||
@@ -83,7 +143,7 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
|
|||||||
onComplete(stepData);
|
onComplete(stepData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [currentStepIndex, steps.length, validateCurrentStep, onComplete, stepData]);
|
}, [currentStep.id, currentStepIndex, steps, validateCurrentStep, onComplete, stepData, updateStepData]);
|
||||||
|
|
||||||
const goToPreviousStep = useCallback(() => {
|
const goToPreviousStep = useCallback(() => {
|
||||||
if (currentStepIndex > 0) {
|
if (currentStepIndex > 0) {
|
||||||
@@ -100,72 +160,186 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderStepIndicator = () => (
|
const renderStepIndicator = () => (
|
||||||
<div className="flex items-center justify-between mb-8">
|
<div className="mb-8" role="navigation" aria-label="Progreso del proceso de configuración">
|
||||||
{steps.map((step, index) => {
|
<div className="max-w-5xl mx-auto px-4">
|
||||||
const isCompleted = completedSteps.has(step.id);
|
|
||||||
const isCurrent = index === currentStepIndex;
|
{/* Progress summary */}
|
||||||
const hasError = validationErrors[step.id];
|
<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 (
|
{/* Progress connections between steps */}
|
||||||
<div
|
<div className="relative mb-8" role="progressbar"
|
||||||
key={step.id}
|
aria-valuenow={completedSteps.size}
|
||||||
className="flex items-center flex-1"
|
aria-valuemin={0}
|
||||||
>
|
aria-valuemax={steps.length}
|
||||||
<button
|
aria-label={`${completedSteps.size} de ${steps.length} pasos completados`}>
|
||||||
onClick={() => goToStep(index)}
|
|
||||||
disabled={index > currentStepIndex + 1}
|
{/* Step circles */}
|
||||||
className={`flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium transition-colors ${
|
<div className="relative">
|
||||||
isCompleted
|
{/* Desktop view */}
|
||||||
? 'bg-green-500 text-white'
|
<div className="hidden md:flex justify-between items-center">
|
||||||
: isCurrent
|
{steps.map((step, index) => {
|
||||||
? hasError
|
const isCompleted = completedSteps.has(step.id);
|
||||||
? 'bg-red-500 text-white'
|
const isCurrent = index === currentStepIndex;
|
||||||
: 'bg-[var(--color-info)]/50 text-white'
|
const hasError = validationErrors[step.id];
|
||||||
: 'bg-[var(--bg-quaternary)] text-[var(--text-secondary)] hover:bg-gray-300'
|
const isAccessible = index <= currentStepIndex + 1;
|
||||||
}`}
|
const nextStepCompleted = index < steps.length - 1 && completedSteps.has(steps[index + 1].id);
|
||||||
>
|
|
||||||
{isCompleted ? '✓' : index + 1}
|
return (
|
||||||
</button>
|
<React.Fragment key={step.id}>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
<div className="ml-3 flex-1">
|
<button
|
||||||
<p className={`text-sm font-medium ${isCurrent ? 'text-[var(--color-info)]' : 'text-[var(--text-secondary)]'}`}>
|
onClick={() => isAccessible ? goToStep(index) : null}
|
||||||
{step.title}
|
disabled={!isAccessible}
|
||||||
{step.isRequired && <span className="text-red-500 ml-1">*</span>}
|
className={`
|
||||||
</p>
|
relative flex items-center justify-center w-12 h-12 rounded-full text-sm font-bold z-10
|
||||||
{hasError && (
|
transition-all duration-300 transform hover:scale-110 focus:outline-none focus:ring-4 focus:ring-opacity-50
|
||||||
<p className="text-xs text-[var(--color-error)] mt-1">{hasError}</p>
|
${
|
||||||
)}
|
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>
|
</div>
|
||||||
|
|
||||||
{index < steps.length - 1 && (
|
{/* Mobile view - simplified horizontal scroll */}
|
||||||
<div className={`flex-1 h-0.5 mx-4 ${
|
<div className="md:hidden">
|
||||||
isCompleted ? 'bg-green-500' : 'bg-[var(--bg-quaternary)]'
|
<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>
|
||||||
</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) {
|
if (!currentStep) {
|
||||||
return (
|
return (
|
||||||
@@ -178,75 +352,92 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
|
|||||||
const StepComponent = currentStep.component;
|
const StepComponent = currentStep.component;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`max-w-4xl mx-auto ${className}`}>
|
<div className={`min-h-screen bg-[var(--bg-primary)] ${className}`}>
|
||||||
{/* Header */}
|
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||||
<div className="flex justify-between items-center mb-6">
|
{/* Header with clean design */}
|
||||||
<div>
|
<div className="text-center mb-6">
|
||||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
|
<h1 className="text-3xl font-bold text-[var(--text-primary)] mb-2">
|
||||||
Configuración inicial
|
Configuración inicial
|
||||||
</h1>
|
</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
|
Completa estos pasos para comenzar a usar la plataforma
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{onExit && (
|
||||||
|
<button
|
||||||
|
onClick={onExit}
|
||||||
|
className="absolute top-8 right-8 text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] text-2xl"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{onExit && (
|
{renderStepIndicator()}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={onExit}
|
|
||||||
className="text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]"
|
|
||||||
>
|
|
||||||
✕ Salir
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{renderProgressBar()}
|
{/* Main Content - Single clean card */}
|
||||||
{renderStepIndicator()}
|
<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 */}
|
{/* Step content */}
|
||||||
<Card className="p-6">
|
<div className="px-8 py-8">
|
||||||
<div className="mb-6">
|
<StepComponent
|
||||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-2">
|
data={{
|
||||||
{currentStep.title}
|
...stepData[currentStep.id],
|
||||||
</h2>
|
// Pass all step data to allow access to previous steps
|
||||||
<p className="text-[var(--text-secondary)]">
|
allStepData: stepData
|
||||||
{currentStep.description}
|
}}
|
||||||
</p>
|
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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,556 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card, Button, Input, Select, Badge } from '../../ui';
|
|
||||||
import type { OnboardingStepProps } from './OnboardingWizard';
|
|
||||||
|
|
||||||
interface SystemConfig {
|
|
||||||
timezone: string;
|
|
||||||
currency: string;
|
|
||||||
language: string;
|
|
||||||
date_format: string;
|
|
||||||
number_format: string;
|
|
||||||
working_hours: {
|
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
days: number[];
|
|
||||||
};
|
|
||||||
notifications: {
|
|
||||||
email_enabled: boolean;
|
|
||||||
sms_enabled: boolean;
|
|
||||||
push_enabled: boolean;
|
|
||||||
alert_preferences: string[];
|
|
||||||
};
|
|
||||||
integrations: {
|
|
||||||
pos_system?: string;
|
|
||||||
accounting_software?: string;
|
|
||||||
payment_provider?: string;
|
|
||||||
};
|
|
||||||
features: {
|
|
||||||
inventory_management: boolean;
|
|
||||||
production_planning: boolean;
|
|
||||||
sales_analytics: boolean;
|
|
||||||
customer_management: boolean;
|
|
||||||
financial_reporting: boolean;
|
|
||||||
quality_control: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const TIMEZONES = [
|
|
||||||
{ value: 'Europe/Madrid', label: 'Madrid (GMT+1)' },
|
|
||||||
{ value: 'Europe/London', label: 'London (GMT+0)' },
|
|
||||||
{ value: 'Europe/Paris', label: 'Paris (GMT+1)' },
|
|
||||||
{ value: 'America/New_York', label: 'New York (GMT-5)' },
|
|
||||||
{ value: 'America/Los_Angeles', label: 'Los Angeles (GMT-8)' },
|
|
||||||
{ value: 'America/Mexico_City', label: 'Mexico City (GMT-6)' },
|
|
||||||
{ value: 'America/Buenos_Aires', label: 'Buenos Aires (GMT-3)' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const CURRENCIES = [
|
|
||||||
{ value: 'EUR', label: 'Euro (€)', symbol: '€' },
|
|
||||||
{ value: 'USD', label: 'US Dollar ($)', symbol: '$' },
|
|
||||||
{ value: 'GBP', label: 'British Pound (£)', symbol: '£' },
|
|
||||||
{ value: 'MXN', label: 'Mexican Peso ($)', symbol: '$' },
|
|
||||||
{ value: 'ARS', label: 'Argentine Peso ($)', symbol: '$' },
|
|
||||||
{ value: 'COP', label: 'Colombian Peso ($)', symbol: '$' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const LANGUAGES = [
|
|
||||||
{ value: 'es', label: 'Español' },
|
|
||||||
{ value: 'en', label: 'English' },
|
|
||||||
{ value: 'fr', label: 'Français' },
|
|
||||||
{ value: 'pt', label: 'Português' },
|
|
||||||
{ value: 'it', label: 'Italiano' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const DAYS_OF_WEEK = [
|
|
||||||
{ value: 1, label: 'Lunes', short: 'L' },
|
|
||||||
{ value: 2, label: 'Martes', short: 'M' },
|
|
||||||
{ value: 3, label: 'Miércoles', short: 'X' },
|
|
||||||
{ value: 4, label: 'Jueves', short: 'J' },
|
|
||||||
{ value: 5, label: 'Viernes', short: 'V' },
|
|
||||||
{ value: 6, label: 'Sábado', short: 'S' },
|
|
||||||
{ value: 0, label: 'Domingo', short: 'D' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const ALERT_TYPES = [
|
|
||||||
{ value: 'low_stock', label: 'Stock bajo', description: 'Cuando los ingredientes están por debajo del mínimo' },
|
|
||||||
{ value: 'production_delays', label: 'Retrasos de producción', description: 'Cuando los lotes se retrasan' },
|
|
||||||
{ value: 'quality_issues', label: 'Problemas de calidad', description: 'Cuando se detectan problemas de calidad' },
|
|
||||||
{ value: 'financial_targets', label: 'Objetivos financieros', description: 'Cuando se alcanzan o no se cumplen objetivos' },
|
|
||||||
{ value: 'equipment_maintenance', label: 'Mantenimiento de equipos', description: 'Recordatorios de mantenimiento' },
|
|
||||||
{ value: 'food_safety', label: 'Seguridad alimentaria', description: 'Alertas relacionadas con seguridad alimentaria' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const FEATURES = [
|
|
||||||
{
|
|
||||||
key: 'inventory_management',
|
|
||||||
title: 'Gestión de inventario',
|
|
||||||
description: 'Control de stock, ingredientes y materias primas',
|
|
||||||
recommended: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'production_planning',
|
|
||||||
title: 'Planificación de producción',
|
|
||||||
description: 'Programación de lotes y gestión de recetas',
|
|
||||||
recommended: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'sales_analytics',
|
|
||||||
title: 'Analytics de ventas',
|
|
||||||
description: 'Reportes y análisis de ventas y tendencias',
|
|
||||||
recommended: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'customer_management',
|
|
||||||
title: 'Gestión de clientes',
|
|
||||||
description: 'Base de datos de clientes y programa de fidelización',
|
|
||||||
recommended: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'financial_reporting',
|
|
||||||
title: 'Reportes financieros',
|
|
||||||
description: 'Análisis de costos, márgenes y rentabilidad',
|
|
||||||
recommended: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'quality_control',
|
|
||||||
title: 'Control de calidad',
|
|
||||||
description: 'Seguimiento de calidad y estándares',
|
|
||||||
recommended: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const SystemSetupStep: React.FC<OnboardingStepProps> = ({
|
|
||||||
data,
|
|
||||||
onDataChange,
|
|
||||||
}) => {
|
|
||||||
const systemData: SystemConfig = {
|
|
||||||
timezone: 'Europe/Madrid',
|
|
||||||
currency: 'EUR',
|
|
||||||
language: 'es',
|
|
||||||
date_format: 'DD/MM/YYYY',
|
|
||||||
number_format: 'European',
|
|
||||||
working_hours: {
|
|
||||||
start: '06:00',
|
|
||||||
end: '20:00',
|
|
||||||
days: [1, 2, 3, 4, 5, 6], // Monday to Saturday
|
|
||||||
},
|
|
||||||
notifications: {
|
|
||||||
email_enabled: true,
|
|
||||||
sms_enabled: false,
|
|
||||||
push_enabled: true,
|
|
||||||
alert_preferences: ['low_stock', 'production_delays', 'quality_issues'],
|
|
||||||
},
|
|
||||||
integrations: {},
|
|
||||||
features: {
|
|
||||||
inventory_management: true,
|
|
||||||
production_planning: true,
|
|
||||||
sales_analytics: true,
|
|
||||||
customer_management: false,
|
|
||||||
financial_reporting: true,
|
|
||||||
quality_control: true,
|
|
||||||
},
|
|
||||||
...data,
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: any) => {
|
|
||||||
onDataChange({
|
|
||||||
...systemData,
|
|
||||||
[field]: value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWorkingHoursChange = (field: string, value: any) => {
|
|
||||||
onDataChange({
|
|
||||||
...systemData,
|
|
||||||
working_hours: {
|
|
||||||
...systemData.working_hours,
|
|
||||||
[field]: value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNotificationChange = (field: string, value: any) => {
|
|
||||||
onDataChange({
|
|
||||||
...systemData,
|
|
||||||
notifications: {
|
|
||||||
...systemData.notifications,
|
|
||||||
[field]: value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleIntegrationChange = (field: string, value: string) => {
|
|
||||||
onDataChange({
|
|
||||||
...systemData,
|
|
||||||
integrations: {
|
|
||||||
...systemData.integrations,
|
|
||||||
[field]: value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFeatureToggle = (feature: string) => {
|
|
||||||
onDataChange({
|
|
||||||
...systemData,
|
|
||||||
features: {
|
|
||||||
...systemData.features,
|
|
||||||
[feature]: !systemData.features[feature as keyof typeof systemData.features],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleWorkingDay = (day: number) => {
|
|
||||||
const currentDays = systemData.working_hours.days;
|
|
||||||
const updatedDays = currentDays.includes(day)
|
|
||||||
? currentDays.filter(d => d !== day)
|
|
||||||
: [...currentDays, day].sort();
|
|
||||||
|
|
||||||
handleWorkingHoursChange('days', updatedDays);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleAlertPreference = (alert: string) => {
|
|
||||||
const current = systemData.notifications.alert_preferences;
|
|
||||||
const updated = current.includes(alert)
|
|
||||||
? current.filter(a => a !== alert)
|
|
||||||
: [...current, alert];
|
|
||||||
|
|
||||||
handleNotificationChange('alert_preferences', updated);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-8">
|
|
||||||
{/* Regional Settings */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
||||||
Configuración regional
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Zona horaria *
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={systemData.timezone}
|
|
||||||
onChange={(e) => handleInputChange('timezone', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{TIMEZONES.map((tz) => (
|
|
||||||
<option key={tz.value} value={tz.value}>
|
|
||||||
{tz.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Moneda *
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={systemData.currency}
|
|
||||||
onChange={(e) => handleInputChange('currency', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{CURRENCIES.map((currency) => (
|
|
||||||
<option key={currency.value} value={currency.value}>
|
|
||||||
{currency.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Idioma *
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={systemData.language}
|
|
||||||
onChange={(e) => handleInputChange('language', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{LANGUAGES.map((lang) => (
|
|
||||||
<option key={lang.value} value={lang.value}>
|
|
||||||
{lang.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Formato de fecha *
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={systemData.date_format}
|
|
||||||
onChange={(e) => handleInputChange('date_format', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<option value="DD/MM/YYYY">DD/MM/YYYY</option>
|
|
||||||
<option value="MM/DD/YYYY">MM/DD/YYYY</option>
|
|
||||||
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Working Hours */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
||||||
Horario de trabajo
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Hora de apertura *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="time"
|
|
||||||
value={systemData.working_hours.start}
|
|
||||||
onChange={(e) => handleWorkingHoursChange('start', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Hora de cierre *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="time"
|
|
||||||
value={systemData.working_hours.end}
|
|
||||||
onChange={(e) => handleWorkingHoursChange('end', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Días de operación *
|
|
||||||
</label>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{DAYS_OF_WEEK.map((day) => (
|
|
||||||
<button
|
|
||||||
key={day.value}
|
|
||||||
onClick={() => toggleWorkingDay(day.value)}
|
|
||||||
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
systemData.working_hours.days.includes(day.value)
|
|
||||||
? 'bg-[var(--color-info)]/50 text-white'
|
|
||||||
: 'bg-[var(--bg-quaternary)] text-[var(--text-secondary)] hover:bg-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{day.short} - {day.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Features */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
||||||
Módulos y características
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)]">
|
|
||||||
Selecciona las características que quieres activar. Podrás cambiar esto más tarde.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{FEATURES.map((feature) => (
|
|
||||||
<Card
|
|
||||||
key={feature.key}
|
|
||||||
className={`p-4 cursor-pointer transition-colors ${
|
|
||||||
systemData.features[feature.key as keyof typeof systemData.features]
|
|
||||||
? 'border-blue-500 bg-[var(--color-info)]/5'
|
|
||||||
: 'border-[var(--border-primary)] hover:border-[var(--border-secondary)]'
|
|
||||||
}`}
|
|
||||||
onClick={() => handleFeatureToggle(feature.key)}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<h4 className="font-medium text-[var(--text-primary)]">{feature.title}</h4>
|
|
||||||
{feature.recommended && (
|
|
||||||
<Badge className="bg-[var(--color-success)]/10 text-[var(--color-success)] text-xs">
|
|
||||||
Recomendado
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)]">{feature.description}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
|
|
||||||
systemData.features[feature.key as keyof typeof systemData.features]
|
|
||||||
? 'border-blue-500 bg-[var(--color-info)]/50'
|
|
||||||
: 'border-[var(--border-secondary)]'
|
|
||||||
}`}>
|
|
||||||
{systemData.features[feature.key as keyof typeof systemData.features] && (
|
|
||||||
<span className="text-white text-xs">✓</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Notifications */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
||||||
Notificaciones
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex flex-wrap gap-4">
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={systemData.notifications.email_enabled}
|
|
||||||
onChange={(e) => handleNotificationChange('email_enabled', e.target.checked)}
|
|
||||||
className="mr-2 rounded"
|
|
||||||
/>
|
|
||||||
Notificaciones por email
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={systemData.notifications.push_enabled}
|
|
||||||
onChange={(e) => handleNotificationChange('push_enabled', e.target.checked)}
|
|
||||||
className="mr-2 rounded"
|
|
||||||
/>
|
|
||||||
Notificaciones push
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={systemData.notifications.sms_enabled}
|
|
||||||
onChange={(e) => handleNotificationChange('sms_enabled', e.target.checked)}
|
|
||||||
className="mr-2 rounded"
|
|
||||||
/>
|
|
||||||
Notificaciones por SMS
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Tipos de alertas que deseas recibir
|
|
||||||
</label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{ALERT_TYPES.map((alert) => (
|
|
||||||
<div
|
|
||||||
key={alert.value}
|
|
||||||
className={`p-3 border rounded-lg cursor-pointer transition-colors ${
|
|
||||||
systemData.notifications.alert_preferences.includes(alert.value)
|
|
||||||
? 'border-blue-500 bg-[var(--color-info)]/5'
|
|
||||||
: 'border-[var(--border-primary)] hover:border-[var(--border-secondary)]'
|
|
||||||
}`}
|
|
||||||
onClick={() => toggleAlertPreference(alert.value)}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h5 className="font-medium text-[var(--text-primary)]">{alert.label}</h5>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)]">{alert.description}</p>
|
|
||||||
</div>
|
|
||||||
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
|
|
||||||
systemData.notifications.alert_preferences.includes(alert.value)
|
|
||||||
? 'border-blue-500 bg-[var(--color-info)]/50'
|
|
||||||
: 'border-[var(--border-secondary)]'
|
|
||||||
}`}>
|
|
||||||
{systemData.notifications.alert_preferences.includes(alert.value) && (
|
|
||||||
<span className="text-white text-xs">✓</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Integrations */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
||||||
Integraciones (opcional)
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)]">
|
|
||||||
Estas integraciones se pueden configurar más tarde desde el panel de administración.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Sistema POS
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={systemData.integrations.pos_system || ''}
|
|
||||||
onChange={(e) => handleIntegrationChange('pos_system', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<option value="">Seleccionar...</option>
|
|
||||||
<option value="square">Square</option>
|
|
||||||
<option value="shopify">Shopify POS</option>
|
|
||||||
<option value="toast">Toast</option>
|
|
||||||
<option value="lightspeed">Lightspeed</option>
|
|
||||||
<option value="other">Otro</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Software de contabilidad
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={systemData.integrations.accounting_software || ''}
|
|
||||||
onChange={(e) => handleIntegrationChange('accounting_software', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<option value="">Seleccionar...</option>
|
|
||||||
<option value="sage">Sage</option>
|
|
||||||
<option value="contaplus">ContaPlus</option>
|
|
||||||
<option value="a3">A3 Software</option>
|
|
||||||
<option value="quickbooks">QuickBooks</option>
|
|
||||||
<option value="other">Otro</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
||||||
Proveedor de pagos
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={systemData.integrations.payment_provider || ''}
|
|
||||||
onChange={(e) => handleIntegrationChange('payment_provider', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<option value="">Seleccionar...</option>
|
|
||||||
<option value="stripe">Stripe</option>
|
|
||||||
<option value="paypal">PayPal</option>
|
|
||||||
<option value="redsys">Redsys</option>
|
|
||||||
<option value="bizum">Bizum</option>
|
|
||||||
<option value="other">Otro</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Summary */}
|
|
||||||
<div className="bg-[var(--bg-secondary)] p-4 rounded-lg">
|
|
||||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Configuración seleccionada</h4>
|
|
||||||
<div className="text-sm text-[var(--text-secondary)] space-y-1">
|
|
||||||
<p><strong>Zona horaria:</strong> {TIMEZONES.find(tz => tz.value === systemData.timezone)?.label}</p>
|
|
||||||
<p><strong>Moneda:</strong> {CURRENCIES.find(c => c.value === systemData.currency)?.label}</p>
|
|
||||||
<p><strong>Horario:</strong> {systemData.working_hours.start} - {systemData.working_hours.end}</p>
|
|
||||||
<p><strong>Días operativos:</strong> {systemData.working_hours.days.length} días por semana</p>
|
|
||||||
<p><strong>Módulos activados:</strong> {Object.values(systemData.features).filter(Boolean).length} de {Object.keys(systemData.features).length}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SystemSetupStep;
|
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
// Onboarding domain components
|
// Onboarding domain components
|
||||||
export { default as OnboardingWizard } from './OnboardingWizard';
|
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
|
// Re-export types from wizard
|
||||||
export type { OnboardingStep, OnboardingStepProps } from './OnboardingWizard';
|
export type { OnboardingStep, OnboardingStepProps } from './OnboardingWizard';
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Store, MapPin, Phone, Mail } from 'lucide-react';
|
||||||
|
import { Button, Card, Input } from '../../../ui';
|
||||||
|
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||||
|
|
||||||
|
export const BakerySetupStep: React.FC<OnboardingStepProps> = ({
|
||||||
|
data,
|
||||||
|
onDataChange,
|
||||||
|
onNext,
|
||||||
|
onPrevious,
|
||||||
|
isFirstStep,
|
||||||
|
isLastStep
|
||||||
|
}) => {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: data.bakery?.name || '',
|
||||||
|
type: data.bakery?.type || '',
|
||||||
|
location: data.bakery?.location || '',
|
||||||
|
phone: data.bakery?.phone || '',
|
||||||
|
email: data.bakery?.email || '',
|
||||||
|
description: data.bakery?.description || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const bakeryTypes = [
|
||||||
|
{
|
||||||
|
value: 'artisan',
|
||||||
|
label: 'Panadería Artesanal',
|
||||||
|
description: 'Producción propia tradicional con recetas artesanales',
|
||||||
|
icon: '🥖'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'industrial',
|
||||||
|
label: 'Panadería Industrial',
|
||||||
|
description: 'Producción a gran escala con procesos automatizados',
|
||||||
|
icon: '🏭'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'retail',
|
||||||
|
label: 'Panadería Retail',
|
||||||
|
description: 'Punto de venta que compra productos terminados',
|
||||||
|
icon: '🏪'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'hybrid',
|
||||||
|
label: 'Modelo Híbrido',
|
||||||
|
description: 'Combina producción propia con productos externos',
|
||||||
|
icon: '🔄'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Update parent data when form changes
|
||||||
|
onDataChange({
|
||||||
|
bakery: {
|
||||||
|
...formData,
|
||||||
|
tenant_id: data.bakery?.tenant_id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [formData]);
|
||||||
|
|
||||||
|
const handleInputChange = (field: string, value: string) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Basic Info */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-3">
|
||||||
|
Nombre de la Panadería *
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
|
placeholder="Ej: Panadería Artesanal El Buen Pan"
|
||||||
|
className="w-full text-lg py-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bakery Type - Simplified */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-4">
|
||||||
|
Tipo de Panadería *
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{bakeryTypes.map((type) => (
|
||||||
|
<label
|
||||||
|
key={type.value}
|
||||||
|
className={`
|
||||||
|
flex items-center p-4 border-2 rounded-lg cursor-pointer transition-all duration-200 hover:shadow-sm
|
||||||
|
${formData.type === type.value
|
||||||
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||||
|
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="bakeryType"
|
||||||
|
value={type.value}
|
||||||
|
checked={formData.type === type.value}
|
||||||
|
onChange={(e) => handleInputChange('type', e.target.value)}
|
||||||
|
className="sr-only"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center space-x-3 w-full">
|
||||||
|
<span className="text-2xl">{type.icon}</span>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)]">
|
||||||
|
{type.label}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
{type.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location and Contact - Simplified */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-3">
|
||||||
|
Ubicación *
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<MapPin className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-[var(--text-tertiary)]" />
|
||||||
|
<Input
|
||||||
|
value={formData.location}
|
||||||
|
onChange={(e) => handleInputChange('location', e.target.value)}
|
||||||
|
placeholder="Dirección completa de tu panadería"
|
||||||
|
className="w-full pl-12 py-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Teléfono (opcional)
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
|
||||||
|
<Input
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||||
|
placeholder="+1 234 567 8900"
|
||||||
|
className="w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Email (opcional)
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
|
||||||
|
<Input
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||||
|
placeholder="contacto@panaderia.com"
|
||||||
|
className="w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Optional: Show loading state when creating tenant */}
|
||||||
|
{data.bakery?.isCreating && (
|
||||||
|
<div className="text-center p-6 bg-[var(--color-primary)]/5 rounded-lg">
|
||||||
|
<div className="animate-spin w-8 h-8 border-3 border-[var(--color-primary)] border-t-transparent rounded-full mx-auto mb-4" />
|
||||||
|
<p className="text-[var(--color-primary)] font-medium">
|
||||||
|
Creando tu espacio de trabajo...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { CheckCircle, Star, Rocket, Gift, Download, Share2, ArrowRight, Calendar } from 'lucide-react';
|
||||||
|
import { Button, Card, Badge } from '../../../ui';
|
||||||
|
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||||
|
|
||||||
|
interface CompletionStats {
|
||||||
|
totalProducts: number;
|
||||||
|
inventoryItems: number;
|
||||||
|
suppliersConfigured: number;
|
||||||
|
mlModelAccuracy: number;
|
||||||
|
estimatedTimeSaved: string;
|
||||||
|
completionScore: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CompletionStep: React.FC<OnboardingStepProps> = ({
|
||||||
|
data,
|
||||||
|
onDataChange,
|
||||||
|
onNext,
|
||||||
|
onPrevious,
|
||||||
|
isFirstStep,
|
||||||
|
isLastStep
|
||||||
|
}) => {
|
||||||
|
const [showConfetti, setShowConfetti] = useState(false);
|
||||||
|
const [completionStats, setCompletionStats] = useState<CompletionStats | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Show confetti animation
|
||||||
|
setShowConfetti(true);
|
||||||
|
const timer = setTimeout(() => setShowConfetti(false), 3000);
|
||||||
|
|
||||||
|
// Calculate completion stats
|
||||||
|
const stats: CompletionStats = {
|
||||||
|
totalProducts: data.detectedProducts?.filter((p: any) => p.status === 'approved').length || 0,
|
||||||
|
inventoryItems: data.inventoryItems?.length || 0,
|
||||||
|
suppliersConfigured: data.suppliers?.length || 0,
|
||||||
|
mlModelAccuracy: data.trainingMetrics?.accuracy * 100 || 0,
|
||||||
|
estimatedTimeSaved: '15-20 horas',
|
||||||
|
completionScore: calculateCompletionScore()
|
||||||
|
};
|
||||||
|
|
||||||
|
setCompletionStats(stats);
|
||||||
|
|
||||||
|
// Update parent data
|
||||||
|
onDataChange({
|
||||||
|
...data,
|
||||||
|
completionStats: stats,
|
||||||
|
onboardingCompleted: true,
|
||||||
|
completedAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const calculateCompletionScore = () => {
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
// Base score for completing setup
|
||||||
|
if (data.bakery?.tenant_id) score += 20;
|
||||||
|
|
||||||
|
// Data upload and analysis
|
||||||
|
if (data.validation?.is_valid) score += 15;
|
||||||
|
if (data.analysisStatus === 'completed') score += 15;
|
||||||
|
|
||||||
|
// Product review
|
||||||
|
const approvedProducts = data.detectedProducts?.filter((p: any) => p.status === 'approved').length || 0;
|
||||||
|
if (approvedProducts > 0) score += 20;
|
||||||
|
|
||||||
|
// Inventory setup
|
||||||
|
if (data.inventoryItems?.length > 0) score += 15;
|
||||||
|
|
||||||
|
// ML training
|
||||||
|
if (data.trainingStatus === 'completed') score += 15;
|
||||||
|
|
||||||
|
return Math.min(score, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateCertificate = () => {
|
||||||
|
// Mock certificate generation
|
||||||
|
const certificateData = {
|
||||||
|
bakeryName: data.bakery?.name || 'Tu Panadería',
|
||||||
|
completionDate: new Date().toLocaleDateString('es-ES'),
|
||||||
|
score: completionStats?.completionScore || 0,
|
||||||
|
features: [
|
||||||
|
'Configuración de Tenant Multi-inquilino',
|
||||||
|
'Análisis de Datos con IA',
|
||||||
|
'Gestión de Inventario Inteligente',
|
||||||
|
'Entrenamiento de Modelo ML',
|
||||||
|
'Integración de Proveedores'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Generating certificate:', certificateData);
|
||||||
|
alert(`🎓 Certificado generado para ${certificateData.bakeryName}\nPuntuación: ${certificateData.score}/100`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleDemo = () => {
|
||||||
|
// Mock demo scheduling
|
||||||
|
alert('📅 Te contactaremos pronto para agendar una demostración personalizada de las funcionalidades avanzadas.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareSuccess = () => {
|
||||||
|
// Mock social sharing
|
||||||
|
const shareText = `¡Acabo de completar la configuración de mi panadería inteligente con IA! 🥖🤖 Puntuación: ${completionStats?.completionScore}/100`;
|
||||||
|
navigator.clipboard.writeText(shareText);
|
||||||
|
alert('✅ Texto copiado al portapapeles. ¡Compártelo en tus redes sociales!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const quickStartActions = [
|
||||||
|
{
|
||||||
|
id: 'dashboard',
|
||||||
|
title: 'Ir al Dashboard',
|
||||||
|
description: 'Explora tu panel de control personalizado',
|
||||||
|
icon: <ArrowRight className="w-5 h-5" />,
|
||||||
|
action: () => onNext(),
|
||||||
|
primary: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'inventory',
|
||||||
|
title: 'Gestionar Inventario',
|
||||||
|
description: 'Revisa y actualiza tu stock actual',
|
||||||
|
icon: <Gift className="w-5 h-5" />,
|
||||||
|
action: () => console.log('Navigate to inventory'),
|
||||||
|
primary: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'predictions',
|
||||||
|
title: 'Ver Predicciones IA',
|
||||||
|
description: 'Consulta las predicciones de demanda',
|
||||||
|
icon: <Rocket className="w-5 h-5" />,
|
||||||
|
action: () => console.log('Navigate to predictions'),
|
||||||
|
primary: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const achievementBadges = [
|
||||||
|
{
|
||||||
|
title: 'Pionero IA',
|
||||||
|
description: 'Primera configuración con Machine Learning',
|
||||||
|
icon: '🤖',
|
||||||
|
earned: data.trainingStatus === 'completed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Organizador',
|
||||||
|
description: 'Inventario completamente configurado',
|
||||||
|
icon: '📦',
|
||||||
|
earned: (data.inventoryItems?.length || 0) >= 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Analista',
|
||||||
|
description: 'Análisis de datos exitoso',
|
||||||
|
icon: '📊',
|
||||||
|
earned: data.analysisStatus === 'completed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Perfeccionista',
|
||||||
|
description: 'Puntuación de configuración 90+',
|
||||||
|
icon: '⭐',
|
||||||
|
earned: (completionStats?.completionScore || 0) >= 90
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Confetti Effect */}
|
||||||
|
{showConfetti && (
|
||||||
|
<div className="fixed inset-0 pointer-events-none z-50 overflow-hidden">
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="text-6xl animate-bounce">🎉</div>
|
||||||
|
</div>
|
||||||
|
{/* Additional confetti elements could be added here */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Celebration Header */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<div className="w-24 h-24 bg-gradient-to-br from-[var(--color-success)] to-[var(--color-primary)] rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg">
|
||||||
|
<CheckCircle className="w-12 h-12 text-white" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-bold text-[var(--text-primary)] mb-4">
|
||||||
|
¡Felicidades! 🎉
|
||||||
|
</h1>
|
||||||
|
<h2 className="text-xl font-semibold text-[var(--color-success)] mb-4">
|
||||||
|
{data.bakery?.name} está listo para funcionar
|
||||||
|
</h2>
|
||||||
|
<p className="text-[var(--text-secondary)] max-w-3xl mx-auto text-lg leading-relaxed">
|
||||||
|
Has completado exitosamente la configuración inicial de tu panadería inteligente.
|
||||||
|
Tu sistema está optimizado con IA y listo para transformar la gestión de tu negocio.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Completion Stats */}
|
||||||
|
{completionStats && (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<div className="inline-flex items-center justify-center w-24 h-24 bg-gradient-to-br from-[var(--color-success)] to-[var(--color-primary)] rounded-full mb-4">
|
||||||
|
<span className="text-2xl font-bold text-white">{completionStats.completionScore}</span>
|
||||||
|
<span className="text-sm text-white/80 ml-1">/100</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||||
|
Puntuación de Configuración
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
¡Excelente trabajo! Has superado el promedio de la industria (78/100)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||||
|
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-info)]">{completionStats.totalProducts}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Productos</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-primary)]">{completionStats.inventoryItems}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Inventario</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-success)]">{completionStats.suppliersConfigured}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Proveedores</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-secondary)]">{completionStats.mlModelAccuracy.toFixed(1)}%</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Precisión IA</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-lg font-bold text-[var(--color-warning)]">{completionStats.estimatedTimeSaved}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Tiempo Ahorrado</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Achievement Badges */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4 text-center">
|
||||||
|
🏆 Logros Desbloqueados
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{achievementBadges.map((badge, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`text-center p-4 rounded-lg border-2 transition-all duration-200 ${
|
||||||
|
badge.earned
|
||||||
|
? 'border-[var(--color-success)]/50 bg-[var(--color-success)]/10'
|
||||||
|
: 'border-[var(--border-secondary)] bg-[var(--bg-secondary)] opacity-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-2xl mb-2">{badge.icon}</div>
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)] mb-1 text-sm">{badge.title}</h4>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">{badge.description}</p>
|
||||||
|
{badge.earned && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<Badge variant="green" className="text-xs">Conseguido</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
|
||||||
|
🚀 Próximos Pasos
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{quickStartActions.map((action) => (
|
||||||
|
<button
|
||||||
|
key={action.id}
|
||||||
|
onClick={action.action}
|
||||||
|
className={`w-full text-left p-4 rounded-lg border transition-all duration-200 hover:shadow-md ${
|
||||||
|
action.primary
|
||||||
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||||
|
: 'border-[var(--border-secondary)] bg-[var(--bg-secondary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||||
|
action.primary ? 'bg-[var(--color-primary)]' : 'bg-[var(--bg-tertiary)]'
|
||||||
|
}`}>
|
||||||
|
<span className={action.primary ? 'text-white' : 'text-[var(--text-secondary)]'}>
|
||||||
|
{action.icon}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className={`font-medium ${
|
||||||
|
action.primary ? 'text-[var(--color-primary)]' : 'text-[var(--text-primary)]'
|
||||||
|
}`}>
|
||||||
|
{action.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">{action.description}</p>
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Additional Actions */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<Download className="w-6 h-6 text-[var(--color-info)] mx-auto mb-2" />
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)] mb-2">Certificado</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-3">
|
||||||
|
Descarga tu certificado de configuración
|
||||||
|
</p>
|
||||||
|
<Button size="sm" variant="outline" onClick={generateCertificate}>
|
||||||
|
Descargar
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<Calendar className="w-6 h-6 text-[var(--color-primary)] mx-auto mb-2" />
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)] mb-2">Demo Personal</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-3">
|
||||||
|
Agenda una demostración 1-a-1
|
||||||
|
</p>
|
||||||
|
<Button size="sm" variant="outline" onClick={scheduleDemo}>
|
||||||
|
Agendar
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<Share2 className="w-6 h-6 text-[var(--color-success)] mx-auto mb-2" />
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)] mb-2">Compartir Éxito</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-3">
|
||||||
|
Comparte tu logro en redes sociales
|
||||||
|
</p>
|
||||||
|
<Button size="sm" variant="outline" onClick={shareSuccess}>
|
||||||
|
Compartir
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary & Thanks */}
|
||||||
|
<Card className="p-6 bg-gradient-to-br from-[var(--color-primary)]/5 to-[var(--color-success)]/5 border-[var(--color-primary)]/20">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-3">
|
||||||
|
🙏 ¡Gracias por confiar en nuestra plataforma!
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)] mb-4">
|
||||||
|
Tu panadería ahora cuenta con tecnología de vanguardia para:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left mb-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2 text-sm">
|
||||||
|
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||||
|
<span>Predicciones de demanda con IA</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 text-sm">
|
||||||
|
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||||
|
<span>Gestión inteligente de inventario</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 text-sm">
|
||||||
|
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||||
|
<span>Optimización de compras</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2 text-sm">
|
||||||
|
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||||
|
<span>Alertas automáticas de restock</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 text-sm">
|
||||||
|
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||||
|
<span>Análisis de tendencias de venta</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 text-sm">
|
||||||
|
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||||
|
<span>Control multi-tenant seguro</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[var(--color-info)]/10 rounded-lg p-4">
|
||||||
|
<p className="text-sm text-[var(--color-info)]">
|
||||||
|
💡 <strong>Consejo:</strong> Explora el dashboard para descubrir todas las funcionalidades disponibles.
|
||||||
|
El sistema aprenderá de tus patrones y mejorará sus recomendaciones con el tiempo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Upload, Brain, CheckCircle, AlertCircle, Download, FileText, Activity, TrendingUp } from 'lucide-react';
|
||||||
|
import { Button, Card, Badge } from '../../../ui';
|
||||||
|
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||||
|
|
||||||
|
type ProcessingStage = 'upload' | 'validating' | 'analyzing' | 'completed' | 'error';
|
||||||
|
|
||||||
|
interface ProcessingResult {
|
||||||
|
// Validation data
|
||||||
|
is_valid: boolean;
|
||||||
|
total_records: number;
|
||||||
|
unique_products: number;
|
||||||
|
product_list: string[];
|
||||||
|
validation_errors: string[];
|
||||||
|
validation_warnings: string[];
|
||||||
|
summary: {
|
||||||
|
date_range: string;
|
||||||
|
total_sales: number;
|
||||||
|
average_daily_sales: number;
|
||||||
|
};
|
||||||
|
// Analysis data
|
||||||
|
productsIdentified: number;
|
||||||
|
categoriesDetected: number;
|
||||||
|
businessModel: string;
|
||||||
|
confidenceScore: number;
|
||||||
|
recommendations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unified mock service that handles both validation and analysis
|
||||||
|
const mockDataProcessingService = {
|
||||||
|
processFile: async (file: File, onProgress: (progress: number, stage: string, message: string) => void) => {
|
||||||
|
return new Promise<ProcessingResult>((resolve, reject) => {
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
const stages = [
|
||||||
|
{ threshold: 20, stage: 'validating', message: 'Validando estructura del archivo...' },
|
||||||
|
{ threshold: 40, stage: 'validating', message: 'Verificando integridad de datos...' },
|
||||||
|
{ threshold: 60, stage: 'analyzing', message: 'Identificando productos únicos...' },
|
||||||
|
{ threshold: 80, stage: 'analyzing', message: 'Analizando patrones de venta...' },
|
||||||
|
{ threshold: 90, stage: 'analyzing', message: 'Generando recomendaciones con IA...' },
|
||||||
|
{ threshold: 100, stage: 'completed', message: 'Procesamiento completado' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (progress < 100) {
|
||||||
|
progress += 10;
|
||||||
|
const currentStage = stages.find(s => progress <= s.threshold);
|
||||||
|
if (currentStage) {
|
||||||
|
onProgress(progress, currentStage.stage, currentStage.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(interval);
|
||||||
|
// Return combined validation + analysis results
|
||||||
|
resolve({
|
||||||
|
// Validation results
|
||||||
|
is_valid: true,
|
||||||
|
total_records: Math.floor(Math.random() * 1000) + 100,
|
||||||
|
unique_products: Math.floor(Math.random() * 50) + 10,
|
||||||
|
product_list: ['Pan Integral', 'Croissant', 'Baguette', 'Empanadas', 'Pan de Centeno', 'Medialunas'],
|
||||||
|
validation_errors: [],
|
||||||
|
validation_warnings: [
|
||||||
|
'Algunas fechas podrían tener formato inconsistente',
|
||||||
|
'3 productos sin categoría definida'
|
||||||
|
],
|
||||||
|
summary: {
|
||||||
|
date_range: '2024-01-01 to 2024-12-31',
|
||||||
|
total_sales: 15420.50,
|
||||||
|
average_daily_sales: 42.25
|
||||||
|
},
|
||||||
|
// Analysis results
|
||||||
|
productsIdentified: 15,
|
||||||
|
categoriesDetected: 4,
|
||||||
|
businessModel: 'artisan',
|
||||||
|
confidenceScore: 94,
|
||||||
|
recommendations: [
|
||||||
|
'Se detectó un modelo de panadería artesanal con producción propia',
|
||||||
|
'Los productos más vendidos son panes tradicionales y bollería',
|
||||||
|
'Recomendamos categorizar el inventario por tipo de producto',
|
||||||
|
'Considera ampliar la línea de productos de repostería'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
|
||||||
|
data,
|
||||||
|
onDataChange,
|
||||||
|
onNext,
|
||||||
|
onPrevious,
|
||||||
|
isFirstStep,
|
||||||
|
isLastStep
|
||||||
|
}) => {
|
||||||
|
const [stage, setStage] = useState<ProcessingStage>(data.processingStage || 'upload');
|
||||||
|
const [uploadedFile, setUploadedFile] = useState<File | null>(data.files?.salesData || null);
|
||||||
|
const [progress, setProgress] = useState(data.processingProgress || 0);
|
||||||
|
const [currentMessage, setCurrentMessage] = useState(data.currentMessage || '');
|
||||||
|
const [results, setResults] = useState<ProcessingResult | null>(data.processingResults || null);
|
||||||
|
const [dragActive, setDragActive] = useState(false);
|
||||||
|
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Update parent data when state changes
|
||||||
|
onDataChange({
|
||||||
|
...data,
|
||||||
|
processingStage: stage,
|
||||||
|
processingProgress: progress,
|
||||||
|
currentMessage: currentMessage,
|
||||||
|
processingResults: results,
|
||||||
|
files: {
|
||||||
|
...data.files,
|
||||||
|
salesData: uploadedFile
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [stage, progress, currentMessage, results, uploadedFile]);
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragActive(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragActive(false);
|
||||||
|
|
||||||
|
const files = Array.from(e.dataTransfer.files);
|
||||||
|
if (files.length > 0) {
|
||||||
|
handleFileUpload(files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
|
handleFileUpload(e.target.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = async (file: File) => {
|
||||||
|
// Validate file type
|
||||||
|
const validExtensions = ['.csv', '.xlsx', '.xls'];
|
||||||
|
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
|
||||||
|
|
||||||
|
if (!validExtensions.includes(fileExtension)) {
|
||||||
|
alert('Formato de archivo no válido. Usa CSV o Excel (.xlsx, .xls)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file size (max 10MB)
|
||||||
|
if (file.size > 10 * 1024 * 1024) {
|
||||||
|
alert('El archivo es demasiado grande. Máximo 10MB permitido.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadedFile(file);
|
||||||
|
setStage('validating');
|
||||||
|
setProgress(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await mockDataProcessingService.processFile(
|
||||||
|
file,
|
||||||
|
(newProgress, newStage, message) => {
|
||||||
|
setProgress(newProgress);
|
||||||
|
setStage(newStage as ProcessingStage);
|
||||||
|
setCurrentMessage(message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setResults(result);
|
||||||
|
setStage('completed');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Processing error:', error);
|
||||||
|
setStage('error');
|
||||||
|
setCurrentMessage('Error en el procesamiento de datos');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadTemplate = () => {
|
||||||
|
const csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
|
||||||
|
2024-01-15,Pan Integral,5,2.50,12.50,Cliente A,Tienda
|
||||||
|
2024-01-15,Croissant,3,1.80,5.40,Cliente B,Online
|
||||||
|
2024-01-15,Baguette,2,3.00,6.00,Cliente C,Tienda
|
||||||
|
2024-01-16,Pan de Centeno,4,2.80,11.20,Cliente A,Tienda
|
||||||
|
2024-01-16,Empanadas,6,4.50,27.00,Cliente D,Delivery`;
|
||||||
|
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', 'plantilla_ventas.csv');
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetProcess = () => {
|
||||||
|
setStage('upload');
|
||||||
|
setUploadedFile(null);
|
||||||
|
setProgress(0);
|
||||||
|
setCurrentMessage('');
|
||||||
|
setResults(null);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Improved Upload Stage */}
|
||||||
|
{stage === 'upload' && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
border-2 border-dashed rounded-2xl p-16 text-center transition-all duration-300 cursor-pointer group
|
||||||
|
${
|
||||||
|
dragActive
|
||||||
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10 scale-[1.02] shadow-lg'
|
||||||
|
: uploadedFile
|
||||||
|
? 'border-[var(--color-success)] bg-[var(--color-success)]/10 shadow-md'
|
||||||
|
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)] hover:bg-[var(--bg-secondary)]/30 hover:shadow-lg'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".csv,.xlsx,.xls"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
{uploadedFile ? (
|
||||||
|
<>
|
||||||
|
<div className="w-20 h-20 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center mx-auto">
|
||||||
|
<CheckCircle className="w-10 h-10 text-[var(--color-success)]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-3xl font-bold text-[var(--color-success)] mb-3">
|
||||||
|
¡Perfecto! Archivo listo
|
||||||
|
</h3>
|
||||||
|
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 inline-block">
|
||||||
|
<p className="text-[var(--text-primary)] font-medium text-lg">
|
||||||
|
📄 {uploadedFile.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-[var(--text-secondary)] text-sm mt-1">
|
||||||
|
{(uploadedFile.size / 1024 / 1024).toFixed(2)} MB • Listo para procesar
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="w-20 h-20 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center mx-auto group-hover:scale-110 transition-transform duration-300">
|
||||||
|
<Upload className="w-10 h-10 text-[var(--color-primary)]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-3xl font-bold text-[var(--text-primary)] mb-4">
|
||||||
|
Sube tu historial de ventas
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)] text-xl leading-relaxed max-w-md mx-auto">
|
||||||
|
Arrastra y suelta tu archivo aquí, o <span className="text-[var(--color-primary)] font-semibold">haz clic para seleccionar</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Visual indicators */}
|
||||||
|
<div className="flex justify-center space-x-8 mt-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mx-auto mb-2">
|
||||||
|
<span className="text-2xl">📊</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">CSV</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mx-auto mb-2">
|
||||||
|
<span className="text-2xl">📈</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">Excel</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mx-auto mb-2">
|
||||||
|
<span className="text-2xl">⚡</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">Hasta 10MB</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-10 px-4 py-2 bg-[var(--bg-secondary)]/50 rounded-lg text-sm text-[var(--text-tertiary)] inline-block">
|
||||||
|
💡 Formatos aceptados: CSV, Excel (XLSX, XLS) • Tamaño máximo: 10MB
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Improved Template Download Section */}
|
||||||
|
<div className="bg-gradient-to-r from-[var(--color-info)]/5 to-[var(--color-primary)]/5 rounded-xl p-6 border border-[var(--color-info)]/20">
|
||||||
|
<div className="flex flex-col md:flex-row items-center space-y-4 md:space-y-0 md:space-x-6">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-[var(--color-info)]/10 flex items-center justify-center flex-shrink-0">
|
||||||
|
<Download className="w-8 h-8 text-[var(--color-info)]" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-center md:text-left">
|
||||||
|
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||||
|
¿Necesitas ayuda con el formato?
|
||||||
|
</h4>
|
||||||
|
<p className="text-[var(--text-secondary)] mb-4">
|
||||||
|
Descarga nuestra plantilla Excel con ejemplos y formato correcto para tus datos de ventas
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={downloadTemplate}
|
||||||
|
className="bg-[var(--color-info)] hover:bg-[var(--color-info)]/90 text-white shadow-lg"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
Descargar Plantilla Gratuita
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Processing Stages */}
|
||||||
|
{(stage === 'validating' || stage === 'analyzing') && (
|
||||||
|
<Card className="p-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="relative mb-8">
|
||||||
|
<div className={`w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6 ${
|
||||||
|
stage === 'validating'
|
||||||
|
? 'bg-[var(--color-info)]/10 animate-pulse'
|
||||||
|
: 'bg-[var(--color-primary)]/10 animate-pulse'
|
||||||
|
}`}>
|
||||||
|
{stage === 'validating' ? (
|
||||||
|
<FileText className={`w-8 h-8 ${stage === 'validating' ? 'text-[var(--color-info)]' : 'text-[var(--color-primary)]'}`} />
|
||||||
|
) : (
|
||||||
|
<Brain className="w-8 h-8 text-[var(--color-primary)]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-2xl font-semibold text-[var(--text-primary)] mb-2">
|
||||||
|
{stage === 'validating' ? 'Validando datos...' : 'Analizando con IA...'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)] mb-8">
|
||||||
|
{currentMessage}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex justify-between items-center mb-3">
|
||||||
|
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||||
|
Progreso
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">{progress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-3">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-r from-[var(--color-info)] to-[var(--color-primary)] h-3 rounded-full transition-all duration-500 ease-out"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Processing Steps */}
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className={`p-4 rounded-lg text-center ${
|
||||||
|
progress >= 40 ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' : 'bg-[var(--bg-secondary)]'
|
||||||
|
}`}>
|
||||||
|
<FileText className="w-6 h-6 mx-auto mb-2" />
|
||||||
|
<span className="text-sm font-medium">Validación</span>
|
||||||
|
</div>
|
||||||
|
<div className={`p-4 rounded-lg text-center ${
|
||||||
|
progress >= 70 ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' : 'bg-[var(--bg-secondary)]'
|
||||||
|
}`}>
|
||||||
|
<Brain className="w-6 h-6 mx-auto mb-2" />
|
||||||
|
<span className="text-sm font-medium">Análisis IA</span>
|
||||||
|
</div>
|
||||||
|
<div className={`p-4 rounded-lg text-center ${
|
||||||
|
progress >= 100 ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' : 'bg-[var(--bg-secondary)]'
|
||||||
|
}`}>
|
||||||
|
<CheckCircle className="w-6 h-6 mx-auto mb-2" />
|
||||||
|
<span className="text-sm font-medium">Completo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Simplified Results Stage */}
|
||||||
|
{stage === 'completed' && results && (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Success Header */}
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 bg-[var(--color-success)] rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<CheckCircle className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-semibold text-[var(--color-success)] mb-3">
|
||||||
|
¡Procesamiento Completado!
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)] max-w-2xl mx-auto">
|
||||||
|
Tus datos han sido procesados exitosamente
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Simple Stats Cards */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="text-center p-4 bg-[var(--color-info)]/10 rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-info)]">{results.total_records}</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Registros</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-4 bg-[var(--color-primary)]/10 rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-primary)]">{results.productsIdentified}</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Productos</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-success)]">{results.confidenceScore}%</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Confianza</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-lg font-bold text-[var(--text-primary)]">
|
||||||
|
{results.businessModel === 'artisan' ? 'Artesanal' :
|
||||||
|
results.businessModel === 'retail' ? 'Retail' : 'Híbrido'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Modelo</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error State */}
|
||||||
|
{stage === 'error' && (
|
||||||
|
<Card className="p-8 text-center">
|
||||||
|
<div className="w-16 h-16 bg-[var(--color-error)] rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<AlertCircle className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-[var(--color-error)] mb-3">
|
||||||
|
Error en el procesamiento
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)] mb-6">
|
||||||
|
{currentMessage}
|
||||||
|
</p>
|
||||||
|
<Button onClick={resetProcess} variant="outline">
|
||||||
|
Intentar nuevamente
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,565 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Package, Calendar, AlertTriangle, Plus, Edit, Trash2 } from 'lucide-react';
|
||||||
|
import { Button, Card, Input, Badge } from '../../../ui';
|
||||||
|
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||||
|
|
||||||
|
interface InventoryItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: 'ingredient' | 'finished_product';
|
||||||
|
current_stock: number;
|
||||||
|
min_stock: number;
|
||||||
|
max_stock: number;
|
||||||
|
unit: string;
|
||||||
|
expiry_date?: string;
|
||||||
|
supplier?: string;
|
||||||
|
cost_per_unit?: number;
|
||||||
|
requires_refrigeration: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock inventory items based on approved products
|
||||||
|
const mockInventoryItems: InventoryItem[] = [
|
||||||
|
{
|
||||||
|
id: '1', name: 'Harina de Trigo', category: 'ingredient',
|
||||||
|
current_stock: 50, min_stock: 20, max_stock: 100, unit: 'kg',
|
||||||
|
expiry_date: '2024-12-31', supplier: 'Molinos del Sur',
|
||||||
|
cost_per_unit: 1.20, requires_refrigeration: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2', name: 'Levadura Fresca', category: 'ingredient',
|
||||||
|
current_stock: 5, min_stock: 2, max_stock: 10, unit: 'kg',
|
||||||
|
expiry_date: '2024-03-15', supplier: 'Levaduras Pro',
|
||||||
|
cost_per_unit: 3.50, requires_refrigeration: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3', name: 'Pan Integral', category: 'finished_product',
|
||||||
|
current_stock: 20, min_stock: 10, max_stock: 50, unit: 'unidades',
|
||||||
|
expiry_date: '2024-01-25', requires_refrigeration: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4', name: 'Mantequilla', category: 'ingredient',
|
||||||
|
current_stock: 15, min_stock: 5, max_stock: 30, unit: 'kg',
|
||||||
|
expiry_date: '2024-02-28', supplier: 'Lácteos Premium',
|
||||||
|
cost_per_unit: 4.20, requires_refrigeration: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
|
||||||
|
data,
|
||||||
|
onDataChange,
|
||||||
|
onNext,
|
||||||
|
onPrevious,
|
||||||
|
isFirstStep,
|
||||||
|
isLastStep
|
||||||
|
}) => {
|
||||||
|
const [items, setItems] = useState<InventoryItem[]>(
|
||||||
|
data.inventoryItems || mockInventoryItems
|
||||||
|
);
|
||||||
|
const [editingItem, setEditingItem] = useState<InventoryItem | null>(null);
|
||||||
|
const [isAddingNew, setIsAddingNew] = useState(false);
|
||||||
|
const [filterCategory, setFilterCategory] = useState<'all' | 'ingredient' | 'finished_product'>('all');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDataChange({
|
||||||
|
...data,
|
||||||
|
inventoryItems: items,
|
||||||
|
inventoryConfigured: items.length > 0 && items.every(item =>
|
||||||
|
item.min_stock > 0 && item.max_stock > item.min_stock
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
const handleAddItem = () => {
|
||||||
|
const newItem: InventoryItem = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: '',
|
||||||
|
category: 'ingredient',
|
||||||
|
current_stock: 0,
|
||||||
|
min_stock: 0,
|
||||||
|
max_stock: 0,
|
||||||
|
unit: 'kg',
|
||||||
|
requires_refrigeration: false
|
||||||
|
};
|
||||||
|
setEditingItem(newItem);
|
||||||
|
setIsAddingNew(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveItem = (item: InventoryItem) => {
|
||||||
|
if (isAddingNew) {
|
||||||
|
setItems(prev => [...prev, item]);
|
||||||
|
} else {
|
||||||
|
setItems(prev => prev.map(i => i.id === item.id ? item : i));
|
||||||
|
}
|
||||||
|
setEditingItem(null);
|
||||||
|
setIsAddingNew(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteItem = (id: string) => {
|
||||||
|
if (window.confirm('¿Estás seguro de eliminar este elemento del inventario?')) {
|
||||||
|
setItems(prev => prev.filter(item => item.id !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuickSetup = () => {
|
||||||
|
// Auto-configure basic inventory based on approved products
|
||||||
|
const autoItems = data.detectedProducts
|
||||||
|
?.filter((p: any) => p.status === 'approved')
|
||||||
|
.map((product: any, index: number) => ({
|
||||||
|
id: `auto_${index}`,
|
||||||
|
name: product.name,
|
||||||
|
category: 'finished_product' as const,
|
||||||
|
current_stock: Math.floor(Math.random() * 20) + 5,
|
||||||
|
min_stock: 5,
|
||||||
|
max_stock: 50,
|
||||||
|
unit: 'unidades',
|
||||||
|
requires_refrigeration: product.category === 'Repostería' || product.category === 'Salados'
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
setItems(prev => [...prev, ...autoItems]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilteredItems = () => {
|
||||||
|
return filterCategory === 'all'
|
||||||
|
? items
|
||||||
|
: items.filter(item => item.category === filterCategory);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStockStatus = (item: InventoryItem) => {
|
||||||
|
if (item.current_stock <= item.min_stock) return { status: 'low', color: 'red', text: 'Stock Bajo' };
|
||||||
|
if (item.current_stock >= item.max_stock) return { status: 'high', color: 'blue', text: 'Stock Alto' };
|
||||||
|
return { status: 'normal', color: 'green', text: 'Normal' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNearExpiry = (expiryDate?: string) => {
|
||||||
|
if (!expiryDate) return false;
|
||||||
|
const expiry = new Date(expiryDate);
|
||||||
|
const today = new Date();
|
||||||
|
const diffDays = (expiry.getTime() - today.getTime()) / (1000 * 3600 * 24);
|
||||||
|
return diffDays <= 7;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
total: items.length,
|
||||||
|
ingredients: items.filter(i => i.category === 'ingredient').length,
|
||||||
|
products: items.filter(i => i.category === 'finished_product').length,
|
||||||
|
lowStock: items.filter(i => i.current_stock <= i.min_stock).length,
|
||||||
|
nearExpiry: items.filter(i => isNearExpiry(i.expiry_date)).length,
|
||||||
|
refrigerated: items.filter(i => i.requires_refrigeration).length
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
{stats.total} elementos configurados
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleQuickSetup}
|
||||||
|
className="text-[var(--color-info)]"
|
||||||
|
>
|
||||||
|
Auto-configurar Productos
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleAddItem}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Agregar Elemento
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-6 gap-4">
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.total}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Total</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.ingredients}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Ingredientes</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-success)]">{stats.products}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Productos</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-error)]">{stats.lowStock}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Stock Bajo</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-warning)]">{stats.nearExpiry}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Por Vencer</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.refrigerated}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Refrigerado</p>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Filtrar:</label>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{[
|
||||||
|
{ value: 'all', label: 'Todos' },
|
||||||
|
{ value: 'ingredient', label: 'Ingredientes' },
|
||||||
|
{ value: 'finished_product', label: 'Productos' }
|
||||||
|
].map(filter => (
|
||||||
|
<button
|
||||||
|
key={filter.value}
|
||||||
|
onClick={() => setFilterCategory(filter.value as any)}
|
||||||
|
className={`px-3 py-1 text-sm rounded-full transition-colors ${
|
||||||
|
filterCategory === filter.value
|
||||||
|
? 'bg-[var(--color-primary)] text-white'
|
||||||
|
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Inventory Items */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{getFilteredItems().map((item) => {
|
||||||
|
const stockStatus = getStockStatus(item);
|
||||||
|
const nearExpiry = isNearExpiry(item.expiry_date);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={item.id} className="p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start space-x-4 flex-1">
|
||||||
|
{/* Category Icon */}
|
||||||
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center mt-1 ${
|
||||||
|
item.category === 'ingredient'
|
||||||
|
? 'bg-[var(--color-primary)]/10'
|
||||||
|
: 'bg-[var(--color-success)]/10'
|
||||||
|
}`}>
|
||||||
|
<Package className={`w-4 h-4 ${
|
||||||
|
item.category === 'ingredient'
|
||||||
|
? 'text-[var(--color-primary)]'
|
||||||
|
: 'text-[var(--color-success)]'
|
||||||
|
}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Item Info */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{item.name}</h4>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Badge variant={item.category === 'ingredient' ? 'blue' : 'green'}>
|
||||||
|
{item.category === 'ingredient' ? 'Ingrediente' : 'Producto'}
|
||||||
|
</Badge>
|
||||||
|
{item.requires_refrigeration && (
|
||||||
|
<Badge variant="gray">❄️ Refrigeración</Badge>
|
||||||
|
)}
|
||||||
|
<Badge variant={stockStatus.color}>
|
||||||
|
{stockStatus.text}
|
||||||
|
</Badge>
|
||||||
|
{nearExpiry && (
|
||||||
|
<Badge variant="red">Vence Pronto</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-6 text-sm text-[var(--text-secondary)]">
|
||||||
|
<div>
|
||||||
|
<span className="text-[var(--text-tertiary)]">Stock Actual: </span>
|
||||||
|
<span className={`font-medium ${stockStatus.status === 'low' ? 'text-[var(--color-error)]' : 'text-[var(--text-primary)]'}`}>
|
||||||
|
{item.current_stock} {item.unit}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-[var(--text-tertiary)]">Rango: </span>
|
||||||
|
<span className="font-medium text-[var(--text-primary)]">
|
||||||
|
{item.min_stock} - {item.max_stock} {item.unit}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.expiry_date && (
|
||||||
|
<div>
|
||||||
|
<span className="text-[var(--text-tertiary)]">Vencimiento: </span>
|
||||||
|
<span className={`font-medium ${nearExpiry ? 'text-[var(--color-error)]' : 'text-[var(--text-primary)]'}`}>
|
||||||
|
{new Date(item.expiry_date).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{item.cost_per_unit && (
|
||||||
|
<div>
|
||||||
|
<span className="text-[var(--text-tertiary)]">Costo/Unidad: </span>
|
||||||
|
<span className="font-medium text-[var(--text-primary)]">
|
||||||
|
${item.cost_per_unit.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex gap-2 ml-4 mt-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setEditingItem(item)}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4 mr-1" />
|
||||||
|
Editar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleDeleteItem(item.id)}
|
||||||
|
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{getFilteredItems().length === 0 && (
|
||||||
|
<Card className="p-8 text-center">
|
||||||
|
<Package className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
|
||||||
|
<p className="text-[var(--text-secondary)]">No hay elementos en esta categoría</p>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warnings */}
|
||||||
|
{(stats.lowStock > 0 || stats.nearExpiry > 0) && (
|
||||||
|
<Card className="p-4 bg-[var(--color-warning-50)] border-[var(--color-warning-200)]">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-[var(--color-warning)] flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-[var(--color-warning-800)] mb-1">Advertencias de Inventario</h4>
|
||||||
|
{stats.lowStock > 0 && (
|
||||||
|
<p className="text-sm text-[var(--color-warning-700)] mb-1">
|
||||||
|
• {stats.lowStock} elemento(s) con stock bajo
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{stats.nearExpiry > 0 && (
|
||||||
|
<p className="text-sm text-[var(--color-warning-700)]">
|
||||||
|
• {stats.nearExpiry} elemento(s) próximos a vencer
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Edit Modal */}
|
||||||
|
{editingItem && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
<Card className="p-6 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
|
||||||
|
{isAddingNew ? 'Agregar Elemento' : 'Editar Elemento'}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<InventoryItemForm
|
||||||
|
item={editingItem}
|
||||||
|
onSave={handleSaveItem}
|
||||||
|
onCancel={() => {
|
||||||
|
setEditingItem(null);
|
||||||
|
setIsAddingNew(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Information */}
|
||||||
|
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
||||||
|
<h4 className="font-medium text-[var(--color-info)] mb-2">
|
||||||
|
📦 Configuración de Inventario:
|
||||||
|
</h4>
|
||||||
|
<ul className="text-sm text-[var(--color-info)] space-y-1">
|
||||||
|
<li>• <strong>Stock Mínimo:</strong> Nivel que dispara alertas de reabastecimiento</li>
|
||||||
|
<li>• <strong>Stock Máximo:</strong> Capacidad máxima de almacenamiento</li>
|
||||||
|
<li>• <strong>Fechas de Vencimiento:</strong> Control automático de productos perecederos</li>
|
||||||
|
<li>• <strong>Refrigeración:</strong> Identifica productos que requieren frío</li>
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Component for editing inventory items
|
||||||
|
interface InventoryItemFormProps {
|
||||||
|
item: InventoryItem;
|
||||||
|
onSave: (item: InventoryItem) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InventoryItemForm: React.FC<InventoryItemFormProps> = ({ item, onSave, onCancel }) => {
|
||||||
|
const [formData, setFormData] = useState(item);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
alert('El nombre es requerido');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (formData.min_stock >= formData.max_stock) {
|
||||||
|
alert('El stock máximo debe ser mayor al mínimo');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSave(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Nombre *
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
placeholder="Nombre del producto/ingrediente"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Categoría *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.category}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value as any }))}
|
||||||
|
className="w-full p-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
>
|
||||||
|
<option value="ingredient">Ingrediente</option>
|
||||||
|
<option value="finished_product">Producto Terminado</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Unidad *
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={formData.unit}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, unit: e.target.value }))}
|
||||||
|
placeholder="kg, unidades, litros..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Stock Actual
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={formData.current_stock}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, current_stock: Number(e.target.value) }))}
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Stock Mín. *
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={formData.min_stock}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, min_stock: Number(e.target.value) }))}
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Stock Máx. *
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={formData.max_stock}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, max_stock: Number(e.target.value) }))}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Fecha Vencimiento
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={formData.expiry_date || ''}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, expiry_date: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Costo por Unidad
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={formData.cost_per_unit || ''}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, cost_per_unit: Number(e.target.value) }))}
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Proveedor
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={formData.supplier || ''}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, supplier: e.target.value }))}
|
||||||
|
placeholder="Nombre del proveedor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="refrigeration"
|
||||||
|
checked={formData.requires_refrigeration}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, requires_refrigeration: e.target.checked }))}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<label htmlFor="refrigeration" className="text-sm text-[var(--text-primary)]">
|
||||||
|
Requiere refrigeración
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3 pt-4">
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
Guardar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,780 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Brain, Activity, Zap, CheckCircle, AlertCircle, TrendingUp } from 'lucide-react';
|
||||||
|
import { Button, Card, Badge } from '../../../ui';
|
||||||
|
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||||
|
|
||||||
|
interface TrainingMetrics {
|
||||||
|
accuracy: number;
|
||||||
|
precision: number;
|
||||||
|
recall: number;
|
||||||
|
f1_score: number;
|
||||||
|
training_loss: number;
|
||||||
|
validation_loss: number;
|
||||||
|
epochs_completed: number;
|
||||||
|
total_epochs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TrainingLog {
|
||||||
|
timestamp: string;
|
||||||
|
message: string;
|
||||||
|
level: 'info' | 'warning' | 'error' | 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced Mock ML training service that matches backend behavior
|
||||||
|
const mockMLService = {
|
||||||
|
startTraining: async (trainingData: any) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let progress = 0;
|
||||||
|
let step_count = 0;
|
||||||
|
const total_steps = 12;
|
||||||
|
|
||||||
|
// Backend-matching training steps and messages
|
||||||
|
const trainingSteps = [
|
||||||
|
{ step: 'data_validation', message: 'Validando datos de entrenamiento...', progress: 10 },
|
||||||
|
{ step: 'data_preparation_start', message: 'Preparando conjunto de datos...', progress: 20 },
|
||||||
|
{ step: 'feature_engineering', message: 'Creando características para el modelo...', progress: 30 },
|
||||||
|
{ step: 'data_preparation_complete', message: 'Preparación de datos completada', progress: 35 },
|
||||||
|
{ step: 'ml_training_start', message: 'Iniciando entrenamiento del modelo Prophet...', progress: 40 },
|
||||||
|
{ step: 'model_fitting', message: 'Ajustando modelo a los datos históricos...', progress: 55 },
|
||||||
|
{ step: 'pattern_detection', message: 'Detectando patrones estacionales y tendencias...', progress: 70 },
|
||||||
|
{ step: 'validation', message: 'Validando precisión del modelo...', progress: 80 },
|
||||||
|
{ step: 'training_complete', message: 'Entrenamiento ML completado', progress: 85 },
|
||||||
|
{ step: 'storing_models', message: 'Guardando modelo entrenado...', progress: 90 },
|
||||||
|
{ step: 'performance_metrics', message: 'Calculando métricas de rendimiento...', progress: 95 },
|
||||||
|
{ step: 'completed', message: 'Tu Asistente Inteligente está listo!', progress: 100 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (step_count >= trainingSteps.length) {
|
||||||
|
clearInterval(interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStep = trainingSteps[step_count];
|
||||||
|
progress = currentStep.progress;
|
||||||
|
|
||||||
|
// Generate realistic metrics that improve over time
|
||||||
|
const stepProgress = step_count / trainingSteps.length;
|
||||||
|
const baseAccuracy = 0.65;
|
||||||
|
const maxAccuracy = 0.93;
|
||||||
|
const currentAccuracy = baseAccuracy + (maxAccuracy - baseAccuracy) * Math.pow(stepProgress, 0.8);
|
||||||
|
|
||||||
|
const metrics: TrainingMetrics = {
|
||||||
|
accuracy: Math.min(maxAccuracy, currentAccuracy + (Math.random() * 0.02 - 0.01)),
|
||||||
|
precision: Math.min(0.95, 0.70 + stepProgress * 0.23 + (Math.random() * 0.02 - 0.01)),
|
||||||
|
recall: Math.min(0.94, 0.68 + stepProgress * 0.24 + (Math.random() * 0.02 - 0.01)),
|
||||||
|
f1_score: Math.min(0.94, 0.69 + stepProgress * 0.23 + (Math.random() * 0.02 - 0.01)),
|
||||||
|
training_loss: Math.max(0.08, 1.2 - stepProgress * 1.0 + (Math.random() * 0.05 - 0.025)),
|
||||||
|
validation_loss: Math.max(0.10, 1.3 - stepProgress * 1.1 + (Math.random() * 0.06 - 0.03)),
|
||||||
|
epochs_completed: Math.floor(stepProgress * 15) + 1,
|
||||||
|
total_epochs: 15
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate step-specific logs that match backend behavior
|
||||||
|
const logs: TrainingLog[] = [];
|
||||||
|
|
||||||
|
// Add the current step message
|
||||||
|
logs.push({
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
message: currentStep.message,
|
||||||
|
level: currentStep.step === 'completed' ? 'success' : 'info'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add specific logs based on the training step
|
||||||
|
if (currentStep.step === 'data_validation') {
|
||||||
|
logs.push({
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
message: `Productos analizados: ${trainingData.products?.length || 3}`,
|
||||||
|
level: 'info'
|
||||||
|
});
|
||||||
|
logs.push({
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
message: `Registros de inventario: ${trainingData.inventory?.length || 3}`,
|
||||||
|
level: 'info'
|
||||||
|
});
|
||||||
|
} else if (currentStep.step === 'ml_training_start') {
|
||||||
|
logs.push({
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
message: 'Usando modelo Prophet optimizado para panadería',
|
||||||
|
level: 'info'
|
||||||
|
});
|
||||||
|
} else if (currentStep.step === 'model_fitting') {
|
||||||
|
logs.push({
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
message: `Precisión actual: ${(metrics.accuracy * 100).toFixed(1)}%`,
|
||||||
|
level: 'info'
|
||||||
|
});
|
||||||
|
} else if (currentStep.step === 'pattern_detection') {
|
||||||
|
logs.push({
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
message: 'Patrones estacionales detectados: días de la semana, horas pico',
|
||||||
|
level: 'success'
|
||||||
|
});
|
||||||
|
} else if (currentStep.step === 'validation') {
|
||||||
|
logs.push({
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
message: `MAE: ${(Math.random() * 2 + 1.5).toFixed(2)}, MAPE: ${((1 - metrics.accuracy) * 100).toFixed(1)}%`,
|
||||||
|
level: 'info'
|
||||||
|
});
|
||||||
|
} else if (currentStep.step === 'storing_models') {
|
||||||
|
logs.push({
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
message: `Modelo guardado: bakery_prophet_${Date.now()}`,
|
||||||
|
level: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit progress event that matches backend WebSocket message structure
|
||||||
|
window.dispatchEvent(new CustomEvent('mlTrainingProgress', {
|
||||||
|
detail: {
|
||||||
|
type: 'progress',
|
||||||
|
job_id: `training_${Date.now()}`,
|
||||||
|
data: {
|
||||||
|
progress,
|
||||||
|
current_step: currentStep.step,
|
||||||
|
step_details: currentStep.message,
|
||||||
|
products_completed: Math.floor(step_count / 3),
|
||||||
|
products_total: Math.max(3, trainingData.products?.length || 3),
|
||||||
|
estimated_time_remaining_minutes: Math.max(0, Math.floor((total_steps - step_count) * 0.75))
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
// Keep old format for backward compatibility
|
||||||
|
status: progress >= 100 ? 'completed' : 'training',
|
||||||
|
currentPhase: currentStep.message,
|
||||||
|
metrics,
|
||||||
|
logs
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
step_count++;
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(interval);
|
||||||
|
|
||||||
|
// Generate final results matching backend response structure
|
||||||
|
const finalAccuracy = 0.89 + Math.random() * 0.05;
|
||||||
|
const finalResult = {
|
||||||
|
success: true,
|
||||||
|
job_id: `training_${Date.now()}`,
|
||||||
|
tenant_id: 'demo_tenant',
|
||||||
|
status: 'completed',
|
||||||
|
training_results: {
|
||||||
|
total_products: Math.max(3, trainingData.products?.length || 3),
|
||||||
|
successful_trainings: Math.max(3, trainingData.products?.length || 3),
|
||||||
|
failed_trainings: 0,
|
||||||
|
products: Array.from({length: Math.max(3, trainingData.products?.length || 3)}, (_, i) => ({
|
||||||
|
inventory_product_id: `product_${i + 1}`,
|
||||||
|
status: 'completed',
|
||||||
|
model_id: `model_${i + 1}_${Date.now()}`,
|
||||||
|
data_points: 45 + Math.floor(Math.random() * 30),
|
||||||
|
metrics: {
|
||||||
|
mape: (1 - finalAccuracy) * 100 + (Math.random() * 2 - 1),
|
||||||
|
mae: 1.5 + Math.random() * 1.0,
|
||||||
|
rmse: 2.1 + Math.random() * 1.2,
|
||||||
|
r2_score: finalAccuracy + (Math.random() * 0.02 - 0.01)
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
overall_training_time_seconds: 45 + Math.random() * 15
|
||||||
|
},
|
||||||
|
data_summary: {
|
||||||
|
sales_records: trainingData.inventory?.length * 30 || 1500,
|
||||||
|
weather_records: 90,
|
||||||
|
traffic_records: 85,
|
||||||
|
date_range: {
|
||||||
|
start: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
end: new Date().toISOString()
|
||||||
|
},
|
||||||
|
data_sources_used: ['bakery_sales', 'weather_forecast', 'madrid_traffic'],
|
||||||
|
constraints_applied: {}
|
||||||
|
},
|
||||||
|
finalMetrics: {
|
||||||
|
accuracy: finalAccuracy,
|
||||||
|
precision: finalAccuracy * 0.98,
|
||||||
|
recall: finalAccuracy * 0.96,
|
||||||
|
f1_score: finalAccuracy * 0.97,
|
||||||
|
training_loss: 0.12 + Math.random() * 0.05,
|
||||||
|
validation_loss: 0.15 + Math.random() * 0.05,
|
||||||
|
total_epochs: 15
|
||||||
|
},
|
||||||
|
modelId: `bakery_prophet_${Date.now()}`,
|
||||||
|
deploymentUrl: '/api/v1/training/models/predict',
|
||||||
|
trainingDuration: `${(45 + Math.random() * 15).toFixed(1)} segundos`,
|
||||||
|
datasetSize: `${trainingData.inventory?.length * 30 || 1500} registros`,
|
||||||
|
modelVersion: '2.1.0',
|
||||||
|
completed_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve(finalResult);
|
||||||
|
}
|
||||||
|
}, 1800); // Matches backend timing
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTrainingPhase = (epoch: number, total: number) => {
|
||||||
|
const progress = epoch / total;
|
||||||
|
if (progress <= 0.3) return 'Inicializando modelo de IA...';
|
||||||
|
if (progress <= 0.6) return 'Entrenando patrones de venta...';
|
||||||
|
if (progress <= 0.8) return 'Optimizando predicciones...';
|
||||||
|
if (progress <= 0.95) return 'Validando rendimiento...';
|
||||||
|
return 'Finalizando entrenamiento...';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
|
||||||
|
data,
|
||||||
|
onDataChange,
|
||||||
|
onNext,
|
||||||
|
onPrevious,
|
||||||
|
isFirstStep,
|
||||||
|
isLastStep
|
||||||
|
}) => {
|
||||||
|
const [trainingStatus, setTrainingStatus] = useState(data.trainingStatus || 'pending');
|
||||||
|
const [progress, setProgress] = useState(data.trainingProgress || 0);
|
||||||
|
const [currentPhase, setCurrentPhase] = useState(data.currentPhase || '');
|
||||||
|
const [metrics, setMetrics] = useState<TrainingMetrics | null>(data.trainingMetrics || null);
|
||||||
|
const [logs, setLogs] = useState<TrainingLog[]>(data.trainingLogs || []);
|
||||||
|
const [finalResults, setFinalResults] = useState<any>(data.finalResults || null);
|
||||||
|
|
||||||
|
// New state variables for backend-compatible data
|
||||||
|
const [productsCompleted, setProductsCompleted] = useState(0);
|
||||||
|
const [productsTotal, setProductsTotal] = useState(0);
|
||||||
|
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Listen for ML training progress updates
|
||||||
|
const handleProgress = (event: CustomEvent) => {
|
||||||
|
const detail = event.detail;
|
||||||
|
|
||||||
|
// Handle new backend-compatible WebSocket message format
|
||||||
|
if (detail.type === 'progress' && detail.data) {
|
||||||
|
const progressData = detail.data;
|
||||||
|
setProgress(progressData.progress || 0);
|
||||||
|
setCurrentPhase(progressData.step_details || progressData.current_step || 'En progreso...');
|
||||||
|
|
||||||
|
// Update products progress if available
|
||||||
|
if (progressData.products_completed !== undefined) {
|
||||||
|
setProductsCompleted(progressData.products_completed);
|
||||||
|
}
|
||||||
|
if (progressData.products_total !== undefined) {
|
||||||
|
setProductsTotal(progressData.products_total);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update time estimate if available
|
||||||
|
if (progressData.estimated_time_remaining_minutes !== undefined) {
|
||||||
|
setEstimatedTimeRemaining(progressData.estimated_time_remaining_minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle metrics (from legacy format for backward compatibility)
|
||||||
|
if (detail.metrics) {
|
||||||
|
setMetrics(detail.metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle logs (from legacy format for backward compatibility)
|
||||||
|
if (detail.logs && detail.logs.length > 0) {
|
||||||
|
setLogs(prev => [...prev, ...detail.logs]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check completion status
|
||||||
|
if (progressData.progress >= 100) {
|
||||||
|
setTrainingStatus('completed');
|
||||||
|
} else {
|
||||||
|
setTrainingStatus('training');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle legacy format for backward compatibility
|
||||||
|
const { progress: newProgress, metrics: newMetrics, logs: newLogs, status, currentPhase: newPhase } = detail;
|
||||||
|
|
||||||
|
setProgress(newProgress || 0);
|
||||||
|
setMetrics(newMetrics);
|
||||||
|
setCurrentPhase(newPhase || 'En progreso...');
|
||||||
|
setTrainingStatus(status || 'training');
|
||||||
|
|
||||||
|
if (newLogs && newLogs.length > 0) {
|
||||||
|
setLogs(prev => [...prev, ...newLogs]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('mlTrainingProgress', handleProgress as EventListener);
|
||||||
|
return () => window.removeEventListener('mlTrainingProgress', handleProgress as EventListener);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Update parent data when state changes
|
||||||
|
onDataChange({
|
||||||
|
...data,
|
||||||
|
trainingStatus,
|
||||||
|
trainingProgress: progress,
|
||||||
|
currentPhase,
|
||||||
|
trainingMetrics: metrics,
|
||||||
|
trainingLogs: logs,
|
||||||
|
finalResults
|
||||||
|
});
|
||||||
|
}, [trainingStatus, progress, currentPhase, metrics, logs, finalResults]);
|
||||||
|
|
||||||
|
// Auto-start training when coming from suppliers step
|
||||||
|
useEffect(() => {
|
||||||
|
if (data.autoStartTraining && trainingStatus === 'pending') {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
startTraining();
|
||||||
|
// Remove the auto-start flag so it doesn't trigger again
|
||||||
|
onDataChange({ ...data, autoStartTraining: false });
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [data.autoStartTraining, trainingStatus]);
|
||||||
|
|
||||||
|
const startTraining = async () => {
|
||||||
|
// Access data from previous steps through allStepData
|
||||||
|
const inventoryData = data.allStepData?.inventory?.inventoryItems ||
|
||||||
|
data.allStepData?.['inventory-setup']?.inventoryItems ||
|
||||||
|
data.inventoryItems || [];
|
||||||
|
|
||||||
|
const detectedProducts = data.allStepData?.review?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
|
||||||
|
data.allStepData?.['data-processing']?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
|
||||||
|
data.detectedProducts?.filter((p: any) => p.status === 'approved') || [];
|
||||||
|
|
||||||
|
const salesValidation = data.allStepData?.['data-processing']?.validation ||
|
||||||
|
data.allStepData?.review?.validation ||
|
||||||
|
data.validation || {};
|
||||||
|
|
||||||
|
// If no data is available, create mock data for demo purposes
|
||||||
|
let finalInventoryData = inventoryData;
|
||||||
|
let finalDetectedProducts = detectedProducts;
|
||||||
|
let finalValidation = salesValidation;
|
||||||
|
|
||||||
|
if (inventoryData.length === 0 && detectedProducts.length === 0) {
|
||||||
|
console.log('No data found from previous steps, using mock data for demo');
|
||||||
|
|
||||||
|
// Create mock data for demonstration
|
||||||
|
finalInventoryData = [
|
||||||
|
{ id: '1', name: 'Harina de Trigo', category: 'ingredient', current_stock: 50, min_stock: 20, max_stock: 100, unit: 'kg' },
|
||||||
|
{ id: '2', name: 'Levadura Fresca', category: 'ingredient', current_stock: 5, min_stock: 2, max_stock: 10, unit: 'kg' },
|
||||||
|
{ id: '3', name: 'Pan Integral', category: 'finished_product', current_stock: 20, min_stock: 10, max_stock: 50, unit: 'unidades' }
|
||||||
|
];
|
||||||
|
|
||||||
|
finalDetectedProducts = [
|
||||||
|
{ name: 'Pan Francés', status: 'approved', category: 'Panadería' },
|
||||||
|
{ name: 'Croissants', status: 'approved', category: 'Repostería' },
|
||||||
|
{ name: 'Pan Integral', status: 'approved', category: 'Panadería' }
|
||||||
|
];
|
||||||
|
|
||||||
|
finalValidation = {
|
||||||
|
total_records: 1500,
|
||||||
|
summary: {
|
||||||
|
date_range: '2024-01-01 to 2024-12-31'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setTrainingStatus('training');
|
||||||
|
setProgress(0);
|
||||||
|
setLogs([{
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
message: 'Iniciando entrenamiento del modelo de Machine Learning...',
|
||||||
|
level: 'info'
|
||||||
|
}]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await mockMLService.startTraining({
|
||||||
|
products: finalDetectedProducts,
|
||||||
|
inventory: finalInventoryData,
|
||||||
|
salesData: finalValidation
|
||||||
|
});
|
||||||
|
|
||||||
|
setFinalResults(result);
|
||||||
|
setTrainingStatus('completed');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ML Training error:', error);
|
||||||
|
setTrainingStatus('error');
|
||||||
|
setLogs(prev => [...prev, {
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
message: 'Error durante el entrenamiento del modelo',
|
||||||
|
level: 'error'
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const retryTraining = () => {
|
||||||
|
setTrainingStatus('pending');
|
||||||
|
setProgress(0);
|
||||||
|
setMetrics(null);
|
||||||
|
setLogs([]);
|
||||||
|
setFinalResults(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLogIcon = (level: string) => {
|
||||||
|
switch (level) {
|
||||||
|
case 'success': return '✅';
|
||||||
|
case 'warning': return '⚠️';
|
||||||
|
case 'error': return '❌';
|
||||||
|
default: return 'ℹ️';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLogColor = (level: string) => {
|
||||||
|
switch (level) {
|
||||||
|
case 'success': return 'text-[var(--color-success)]';
|
||||||
|
case 'warning': return 'text-[var(--color-warning)]';
|
||||||
|
case 'error': return 'text-[var(--color-error)]';
|
||||||
|
default: return 'text-[var(--text-secondary)]';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<div className="w-16 h-16 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Brain className="w-8 h-8 text-[var(--color-primary)]" />
|
||||||
|
</div>
|
||||||
|
<p className="text-[var(--text-secondary)]">
|
||||||
|
Tu asistente inteligente analizará tus datos históricos para ayudarte a tomar mejores decisiones de negocio
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Auto-start notification */}
|
||||||
|
{data.autoStartTraining && trainingStatus === 'pending' && (
|
||||||
|
<Card className="p-4 bg-[var(--color-info)]/10 border-[var(--color-info)]/20 mb-4">
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
<div className="animate-spin w-5 h-5 border-2 border-[var(--color-info)] border-t-transparent rounded-full"></div>
|
||||||
|
<p className="text-[var(--color-info)] font-medium">
|
||||||
|
Creando tu asistente inteligente automáticamente...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Training Controls */}
|
||||||
|
{trainingStatus === 'pending' && !data.autoStartTraining && (
|
||||||
|
<Card className="p-6 text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
|
||||||
|
Crear tu Asistente Inteligente
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)] mb-6">
|
||||||
|
Analizaremos tus datos de ventas y productos para crear un asistente que te ayude a predecir demanda y optimizar tu inventario.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
|
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<Activity className="w-6 h-6 text-[var(--color-info)] mx-auto mb-2" />
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)]">Predicción de Ventas</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Anticipa cuánto vas a vender</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<TrendingUp className="w-6 h-6 text-[var(--color-success)] mx-auto mb-2" />
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)]">Recomendaciones</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Cuánto producir y comprar</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<Zap className="w-6 h-6 text-[var(--color-warning)] mx-auto mb-2" />
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)]">Alertas Automáticas</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Te avisa cuando reordenar</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[var(--color-info)]/10 border border-[var(--color-info)]/20 rounded-lg p-4 mb-6">
|
||||||
|
<h4 className="font-medium text-[var(--color-info)] mb-2">Información que analizaremos:</h4>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-[var(--color-info)]">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Productos:</span>
|
||||||
|
<br />
|
||||||
|
{(() => {
|
||||||
|
const allStepData = data.allStepData || {};
|
||||||
|
const detectedProducts = allStepData.review?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
|
||||||
|
allStepData['data-processing']?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
|
||||||
|
data.detectedProducts?.filter((p: any) => p.status === 'approved') || [];
|
||||||
|
return detectedProducts.length || 3; // fallback to mock data count
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Inventario:</span>
|
||||||
|
<br />
|
||||||
|
{(() => {
|
||||||
|
const allStepData = data.allStepData || {};
|
||||||
|
const inventoryData = allStepData.inventory?.inventoryItems ||
|
||||||
|
allStepData['inventory-setup']?.inventoryItems ||
|
||||||
|
allStepData['inventory']?.inventoryItems ||
|
||||||
|
data.inventoryItems || [];
|
||||||
|
return inventoryData.length || 3; // fallback to mock data count
|
||||||
|
})()} elementos
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Registros:</span>
|
||||||
|
<br />
|
||||||
|
{(() => {
|
||||||
|
const allStepData = data.allStepData || {};
|
||||||
|
const validation = allStepData['data-processing']?.validation ||
|
||||||
|
allStepData.review?.validation ||
|
||||||
|
data.validation || {};
|
||||||
|
return validation.total_records || 1500; // fallback to mock data
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Período:</span>
|
||||||
|
<br />
|
||||||
|
{(() => {
|
||||||
|
const allStepData = data.allStepData || {};
|
||||||
|
const validation = allStepData['data-processing']?.validation ||
|
||||||
|
allStepData.review?.validation ||
|
||||||
|
data.validation || {};
|
||||||
|
const dateRange = validation.summary?.date_range || '2024-01-01 to 2024-12-31';
|
||||||
|
return dateRange.split(' to ').map((date: string) =>
|
||||||
|
new Date(date).toLocaleDateString()
|
||||||
|
).join(' - ');
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={startTraining} className="px-8 py-2">
|
||||||
|
<Brain className="w-4 h-4 mr-2" />
|
||||||
|
Crear mi Asistente Inteligente
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Training Progress */}
|
||||||
|
{trainingStatus === 'training' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<div className="animate-pulse w-12 h-12 bg-[var(--color-primary)] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Brain className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||||
|
Creando tu Asistente Inteligente...
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)]">{currentPhase}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||||
|
Progreso de Creación
|
||||||
|
</span>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">
|
||||||
|
{progress.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
{productsTotal > 0 && (
|
||||||
|
<div className="text-xs text-[var(--text-tertiary)]">
|
||||||
|
{productsCompleted}/{productsTotal} productos
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{estimatedTimeRemaining !== null && estimatedTimeRemaining > 0 && (
|
||||||
|
<div className="text-xs text-[var(--text-tertiary)]">
|
||||||
|
~{estimatedTimeRemaining} min restantes
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-4">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-info)] h-4 rounded-full transition-all duration-1000 relative overflow-hidden"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current Metrics */}
|
||||||
|
{metrics && (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-xl font-bold text-[var(--color-success)]">
|
||||||
|
{(metrics.accuracy * 100).toFixed(1)}%
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Precisión</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-xl font-bold text-[var(--color-info)]">
|
||||||
|
{metrics.training_loss.toFixed(3)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Loss</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-xl font-bold text-[var(--color-primary)]">
|
||||||
|
{metrics.epochs_completed}/{metrics.total_epochs}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Epochs</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-xl font-bold text-[var(--color-secondary)]">
|
||||||
|
{(metrics.f1_score * 100).toFixed(1)}%
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">F1-Score</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Training Logs */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)] mb-4">Progreso del Asistente</h4>
|
||||||
|
<div className="bg-black rounded-lg p-4 max-h-48 overflow-y-auto font-mono text-sm">
|
||||||
|
{logs.slice(-10).map((log, index) => (
|
||||||
|
<div key={index} className={`mb-1 ${getLogColor(log.level)}`}>
|
||||||
|
<span className="text-[var(--text-quaternary)]">[{log.timestamp}]</span>
|
||||||
|
<span className="ml-2">{getLogIcon(log.level)} {log.message}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Training Completed */}
|
||||||
|
{trainingStatus === 'completed' && finalResults && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<div className="w-12 h-12 bg-[var(--color-success)] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<CheckCircle className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--color-success)] mb-2">
|
||||||
|
¡Tu Asistente Inteligente está Listo!
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)]">
|
||||||
|
Tu asistente personalizado está listo para ayudarte con predicciones y recomendaciones
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Final Metrics */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
|
||||||
|
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-success)]">
|
||||||
|
{(finalResults.finalMetrics.accuracy * 100).toFixed(1)}%
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Exactitud</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-[var(--color-info)]/10 rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-info)]">
|
||||||
|
{(finalResults.finalMetrics.precision * 100).toFixed(1)}%
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Confiabilidad</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-[var(--color-primary)]/10 rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-primary)]">
|
||||||
|
{(finalResults.finalMetrics.recall * 100).toFixed(1)}%
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Cobertura</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-[var(--color-secondary-100)] rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-secondary)]">
|
||||||
|
{(finalResults.finalMetrics.f1_score * 100).toFixed(1)}%
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Rendimiento</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-[var(--color-warning-100)] rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-warning)]">
|
||||||
|
{finalResults.finalMetrics.total_epochs}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Iteraciones</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Model Info */}
|
||||||
|
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 mb-6">
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)] mb-3">Información del Modelo:</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-[var(--text-tertiary)]">ID del Modelo:</span>
|
||||||
|
<p className="font-mono text-[var(--text-primary)]">{finalResults.modelId}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-[var(--text-tertiary)]">Versión:</span>
|
||||||
|
<p className="font-mono text-[var(--text-primary)]">{finalResults.modelVersion}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-[var(--text-tertiary)]">Duración:</span>
|
||||||
|
<p className="text-[var(--text-primary)]">{finalResults.trainingDuration}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-[var(--text-tertiary)]">Dataset:</span>
|
||||||
|
<p className="text-[var(--text-primary)]">{finalResults.datasetSize}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-[var(--text-tertiary)]">Endpoint:</span>
|
||||||
|
<p className="font-mono text-[var(--text-primary)]">{finalResults.deploymentUrl}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-[var(--text-tertiary)]">Estado:</span>
|
||||||
|
<p className="text-[var(--color-success)] font-medium">✓ Activo</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Capabilities */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 border border-[var(--color-success)]/20 rounded-lg">
|
||||||
|
<TrendingUp className="w-6 h-6 text-[var(--color-success)] mb-2" />
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)] mb-1">Predicción de Ventas</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Te dice cuánto vas a vender cada día
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border border-[var(--color-info)]/20 rounded-lg">
|
||||||
|
<Activity className="w-6 h-6 text-[var(--color-info)] mb-2" />
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)] mb-1">Recomendaciones</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Te sugiere cuánto producir y comprar
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border border-[var(--color-warning)]/20 rounded-lg">
|
||||||
|
<Zap className="w-6 h-6 text-[var(--color-warning)] mb-2" />
|
||||||
|
<h4 className="font-medium text-[var(--text-primary)] mb-1">Alertas Automáticas</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Te avisa cuando necesitas reordenar
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Training Error */}
|
||||||
|
{trainingStatus === 'error' && (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-[var(--color-error)] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<AlertCircle className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--color-error)] mb-2">
|
||||||
|
Error al Crear el Asistente
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)] mb-4">
|
||||||
|
Ocurrió un problema durante la creación de tu asistente inteligente. Por favor, intenta nuevamente.
|
||||||
|
</p>
|
||||||
|
<Button onClick={retryTraining} variant="outline">
|
||||||
|
Intentar Nuevamente
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Information */}
|
||||||
|
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
||||||
|
<h4 className="font-medium text-[var(--color-info)] mb-2">
|
||||||
|
🎯 ¿Cómo funciona tu Asistente Inteligente?
|
||||||
|
</h4>
|
||||||
|
<ul className="text-sm text-[var(--color-info)] space-y-1">
|
||||||
|
<li>• <strong>Analiza tus ventas:</strong> Estudia tus datos históricos para encontrar patrones</li>
|
||||||
|
<li>• <strong>Entiende tu negocio:</strong> Aprende sobre temporadas altas y bajas</li>
|
||||||
|
<li>• <strong>Hace predicciones:</strong> Te dice cuánto vas a vender cada día</li>
|
||||||
|
<li>• <strong>Da recomendaciones:</strong> Te sugiere cuánto producir y comprar</li>
|
||||||
|
<li>• <strong>Mejora con el tiempo:</strong> Se hace más inteligente con cada venta nueva</li>
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
285
frontend/src/components/domain/onboarding/steps/ReviewStep.tsx
Normal file
285
frontend/src/components/domain/onboarding/steps/ReviewStep.tsx
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Eye, CheckCircle, AlertCircle, Edit, Trash2 } from 'lucide-react';
|
||||||
|
import { Button, Card, Badge } from '../../../ui';
|
||||||
|
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
confidence: number;
|
||||||
|
sales_count: number;
|
||||||
|
estimated_price: number;
|
||||||
|
status: 'approved' | 'rejected' | 'pending';
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock detected products
|
||||||
|
const mockDetectedProducts: Product[] = [
|
||||||
|
{ id: '1', name: 'Pan Integral', category: 'Panadería', confidence: 95, sales_count: 45, estimated_price: 2.50, status: 'pending' },
|
||||||
|
{ id: '2', name: 'Croissant', category: 'Bollería', confidence: 92, sales_count: 38, estimated_price: 1.80, status: 'pending' },
|
||||||
|
{ id: '3', name: 'Baguette', category: 'Panadería', confidence: 88, sales_count: 22, estimated_price: 3.00, status: 'pending' },
|
||||||
|
{ id: '4', name: 'Empanada de Pollo', category: 'Salados', confidence: 85, sales_count: 31, estimated_price: 4.50, status: 'pending' },
|
||||||
|
{ id: '5', name: 'Tarta de Manzana', category: 'Repostería', confidence: 78, sales_count: 12, estimated_price: 15.00, status: 'pending' },
|
||||||
|
{ id: '6', name: 'Pan de Centeno', category: 'Panadería', confidence: 91, sales_count: 18, estimated_price: 2.80, status: 'pending' },
|
||||||
|
{ id: '7', name: 'Medialunas', category: 'Bollería', confidence: 87, sales_count: 29, estimated_price: 1.20, status: 'pending' },
|
||||||
|
{ id: '8', name: 'Sandwich Mixto', category: 'Salados', confidence: 82, sales_count: 25, estimated_price: 5.50, status: 'pending' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ReviewStep: React.FC<OnboardingStepProps> = ({
|
||||||
|
data,
|
||||||
|
onDataChange,
|
||||||
|
onNext,
|
||||||
|
onPrevious,
|
||||||
|
isFirstStep,
|
||||||
|
isLastStep
|
||||||
|
}) => {
|
||||||
|
// Generate products from processing results or use mock data
|
||||||
|
const generateProductsFromResults = (results: any) => {
|
||||||
|
if (!results?.product_list) return mockDetectedProducts;
|
||||||
|
|
||||||
|
return results.product_list.map((name: string, index: number) => ({
|
||||||
|
id: (index + 1).toString(),
|
||||||
|
name,
|
||||||
|
category: index < 3 ? 'Panadería' : index < 5 ? 'Bollería' : 'Salados',
|
||||||
|
confidence: Math.max(75, results.confidenceScore - Math.random() * 15),
|
||||||
|
sales_count: Math.floor(Math.random() * 50) + 10,
|
||||||
|
estimated_price: Math.random() * 5 + 1.5,
|
||||||
|
status: 'pending' as const
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const [products, setProducts] = useState<Product[]>(
|
||||||
|
data.detectedProducts || generateProductsFromResults(data.processingResults)
|
||||||
|
);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
||||||
|
|
||||||
|
const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDataChange({
|
||||||
|
...data,
|
||||||
|
detectedProducts: products,
|
||||||
|
reviewCompleted: products.every(p => p.status !== 'pending')
|
||||||
|
});
|
||||||
|
}, [products]);
|
||||||
|
|
||||||
|
const handleProductAction = (productId: string, action: 'approve' | 'reject') => {
|
||||||
|
setProducts(prev => prev.map(product =>
|
||||||
|
product.id === productId
|
||||||
|
? { ...product, status: action === 'approve' ? 'approved' : 'rejected' }
|
||||||
|
: product
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkAction = (action: 'approve' | 'reject') => {
|
||||||
|
const filteredProducts = getFilteredProducts();
|
||||||
|
setProducts(prev => prev.map(product =>
|
||||||
|
filteredProducts.some(fp => fp.id === product.id)
|
||||||
|
? { ...product, status: action === 'approve' ? 'approved' : 'rejected' }
|
||||||
|
: product
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilteredProducts = () => {
|
||||||
|
if (selectedCategory === 'all') {
|
||||||
|
return products;
|
||||||
|
}
|
||||||
|
return products.filter(p => p.category === selectedCategory);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
total: products.length,
|
||||||
|
approved: products.filter(p => p.status === 'approved').length,
|
||||||
|
rejected: products.filter(p => p.status === 'rejected').length,
|
||||||
|
pending: products.filter(p => p.status === 'pending').length
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Summary Stats */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.total}</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Total</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-success)]">{stats.approved}</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Aprobados</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-[var(--color-error)]/10 rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-error)]">{stats.rejected}</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Rechazados</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-[var(--color-warning)]/10 rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-warning)]">{stats.pending}</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Pendientes</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters and Actions */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-2">
|
||||||
|
Filtrar por categoría:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedCategory}
|
||||||
|
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||||
|
className="border border-[var(--border-secondary)] rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
|
||||||
|
>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<option key={cat} value={cat}>
|
||||||
|
{cat === 'all' ? 'Todas las categorías' : cat}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleBulkAction('approve')}
|
||||||
|
className="text-[var(--color-success)] border-[var(--color-success)] hover:bg-[var(--color-success)]/10"
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-1" />
|
||||||
|
Aprobar Visibles
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleBulkAction('reject')}
|
||||||
|
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-1" />
|
||||||
|
Rechazar Visibles
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Products List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{getFilteredProducts().map((product) => (
|
||||||
|
<Card key={product.id} className="p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start space-x-4 flex-1">
|
||||||
|
{/* Status Icon */}
|
||||||
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center mt-1 ${
|
||||||
|
product.status === 'approved'
|
||||||
|
? 'bg-[var(--color-success)]'
|
||||||
|
: product.status === 'rejected'
|
||||||
|
? 'bg-[var(--color-error)]'
|
||||||
|
: 'bg-[var(--bg-secondary)] border border-[var(--border-secondary)]'
|
||||||
|
}`}>
|
||||||
|
{product.status === 'approved' ? (
|
||||||
|
<CheckCircle className="w-4 h-4 text-white" />
|
||||||
|
) : product.status === 'rejected' ? (
|
||||||
|
<Trash2 className="w-4 h-4 text-white" />
|
||||||
|
) : (
|
||||||
|
<Eye className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Info */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{product.name}</h4>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Badge variant="gray">{product.category}</Badge>
|
||||||
|
{product.status !== 'pending' && (
|
||||||
|
<Badge variant={product.status === 'approved' ? 'green' : 'red'}>
|
||||||
|
{product.status === 'approved' ? 'Aprobado' : 'Rechazado'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
|
||||||
|
<span className={`px-2 py-1 rounded text-xs ${
|
||||||
|
product.confidence >= 90
|
||||||
|
? 'bg-[var(--color-success)]/10 text-[var(--color-success)]'
|
||||||
|
: product.confidence >= 75
|
||||||
|
? 'bg-[var(--color-warning)]/10 text-[var(--color-warning)]'
|
||||||
|
: 'bg-[var(--color-error)]/10 text-[var(--color-error)]'
|
||||||
|
}`}>
|
||||||
|
{product.confidence}% confianza
|
||||||
|
</span>
|
||||||
|
<span>{product.sales_count} ventas</span>
|
||||||
|
<span>${product.estimated_price.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex gap-2 ml-4 mt-1">
|
||||||
|
{product.status === 'pending' ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleProductAction(product.id, 'approve')}
|
||||||
|
className="bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white"
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-1" />
|
||||||
|
Aprobar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleProductAction(product.id, 'reject')}
|
||||||
|
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-1" />
|
||||||
|
Rechazar
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setProducts(prev => prev.map(p => p.id === product.id ? {...p, status: 'pending'} : p))}
|
||||||
|
className="text-[var(--text-secondary)] hover:text-[var(--color-primary)]"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4 mr-1" />
|
||||||
|
Modificar
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Indicator */}
|
||||||
|
{stats.pending > 0 && (
|
||||||
|
<Card className="p-4 bg-[var(--color-warning)]/5 border-[var(--color-warning)]/20">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<AlertCircle className="w-5 h-5 text-[var(--color-warning)]" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-[var(--text-primary)]">
|
||||||
|
{stats.pending} productos pendientes de revisión
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Revisa todos los productos antes de continuar al siguiente paso
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Help Information */}
|
||||||
|
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
||||||
|
<h4 className="font-medium text-[var(--color-info)] mb-3">
|
||||||
|
💡 Consejos para la revisión:
|
||||||
|
</h4>
|
||||||
|
<ul className="text-sm text-[var(--color-info)] space-y-1">
|
||||||
|
<li>• <strong>Confianza alta (90%+):</strong> Productos identificados con alta precisión</li>
|
||||||
|
<li>• <strong>Confianza media (75-89%):</strong> Revisar nombres y categorías</li>
|
||||||
|
<li>• <strong>Confianza baja (<75%):</strong> Verificar que corresponden a tu catálogo</li>
|
||||||
|
<li>• Usa las acciones masivas para aprobar/rechazar por categoría completa</li>
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,585 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Truck, Phone, Mail, Plus, Edit, Trash2, MapPin } from 'lucide-react';
|
||||||
|
import { Button, Card, Input, Badge } from '../../../ui';
|
||||||
|
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||||
|
|
||||||
|
interface Supplier {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
contact_person: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
address: string;
|
||||||
|
categories: string[];
|
||||||
|
payment_terms: string;
|
||||||
|
delivery_days: string[];
|
||||||
|
min_order_amount?: number;
|
||||||
|
notes?: string;
|
||||||
|
status: 'active' | 'inactive';
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock suppliers
|
||||||
|
const mockSuppliers: Supplier[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Molinos del Sur',
|
||||||
|
contact_person: 'Juan Pérez',
|
||||||
|
phone: '+1 555-0123',
|
||||||
|
email: 'ventas@molinosdelsur.com',
|
||||||
|
address: 'Av. Industrial 123, Zona Sur',
|
||||||
|
categories: ['Harinas', 'Granos'],
|
||||||
|
payment_terms: '30 días',
|
||||||
|
delivery_days: ['Lunes', 'Miércoles', 'Viernes'],
|
||||||
|
min_order_amount: 200,
|
||||||
|
notes: 'Proveedor principal de harinas, muy confiable',
|
||||||
|
status: 'active',
|
||||||
|
created_at: '2024-01-15'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Lácteos Premium',
|
||||||
|
contact_person: 'María González',
|
||||||
|
phone: '+1 555-0456',
|
||||||
|
email: 'pedidos@lacteospremium.com',
|
||||||
|
address: 'Calle Central 456, Centro',
|
||||||
|
categories: ['Lácteos', 'Mantequillas', 'Quesos'],
|
||||||
|
payment_terms: '15 días',
|
||||||
|
delivery_days: ['Martes', 'Jueves', 'Sábado'],
|
||||||
|
min_order_amount: 150,
|
||||||
|
status: 'active',
|
||||||
|
created_at: '2024-01-20'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const daysOfWeek = [
|
||||||
|
'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'
|
||||||
|
];
|
||||||
|
|
||||||
|
const commonCategories = [
|
||||||
|
'Harinas', 'Lácteos', 'Levaduras', 'Azúcares', 'Grasas', 'Huevos',
|
||||||
|
'Frutas', 'Chocolates', 'Frutos Secos', 'Especias', 'Conservantes'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SuppliersStep: React.FC<OnboardingStepProps> = ({
|
||||||
|
data,
|
||||||
|
onDataChange,
|
||||||
|
onNext,
|
||||||
|
onPrevious,
|
||||||
|
isFirstStep,
|
||||||
|
isLastStep
|
||||||
|
}) => {
|
||||||
|
const [suppliers, setSuppliers] = useState<Supplier[]>(
|
||||||
|
data.suppliers || mockSuppliers
|
||||||
|
);
|
||||||
|
const [editingSupplier, setEditingSupplier] = useState<Supplier | null>(null);
|
||||||
|
const [isAddingNew, setIsAddingNew] = useState(false);
|
||||||
|
const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'inactive'>('all');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDataChange({
|
||||||
|
...data,
|
||||||
|
suppliers: suppliers,
|
||||||
|
suppliersConfigured: true // This step is optional
|
||||||
|
});
|
||||||
|
}, [suppliers]);
|
||||||
|
|
||||||
|
const handleAddSupplier = () => {
|
||||||
|
const newSupplier: Supplier = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: '',
|
||||||
|
contact_person: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
address: '',
|
||||||
|
categories: [],
|
||||||
|
payment_terms: '30 días',
|
||||||
|
delivery_days: [],
|
||||||
|
status: 'active',
|
||||||
|
created_at: new Date().toISOString().split('T')[0]
|
||||||
|
};
|
||||||
|
setEditingSupplier(newSupplier);
|
||||||
|
setIsAddingNew(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveSupplier = (supplier: Supplier) => {
|
||||||
|
if (isAddingNew) {
|
||||||
|
setSuppliers(prev => [...prev, supplier]);
|
||||||
|
} else {
|
||||||
|
setSuppliers(prev => prev.map(s => s.id === supplier.id ? supplier : s));
|
||||||
|
}
|
||||||
|
setEditingSupplier(null);
|
||||||
|
setIsAddingNew(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteSupplier = (id: string) => {
|
||||||
|
if (window.confirm('¿Estás seguro de eliminar este proveedor?')) {
|
||||||
|
setSuppliers(prev => prev.filter(s => s.id !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSupplierStatus = (id: string) => {
|
||||||
|
setSuppliers(prev => prev.map(s =>
|
||||||
|
s.id === id
|
||||||
|
? { ...s, status: s.status === 'active' ? 'inactive' : 'active' }
|
||||||
|
: s
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilteredSuppliers = () => {
|
||||||
|
return filterStatus === 'all'
|
||||||
|
? suppliers
|
||||||
|
: suppliers.filter(s => s.status === filterStatus);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
total: suppliers.length,
|
||||||
|
active: suppliers.filter(s => s.status === 'active').length,
|
||||||
|
inactive: suppliers.filter(s => s.status === 'inactive').length,
|
||||||
|
categories: Array.from(new Set(suppliers.flatMap(s => s.categories))).length
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Optional Step Notice */}
|
||||||
|
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
||||||
|
<p className="text-sm text-[var(--color-info)]">
|
||||||
|
💡 <strong>Paso Opcional:</strong> Puedes saltar este paso y configurar proveedores más tarde desde el módulo de compras
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
{stats.total} proveedores configurados ({stats.active} activos)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleAddSupplier}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Agregar Proveedor
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.total}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Total</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-success)]">{stats.active}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Activos</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-[var(--text-tertiary)]">{stats.inactive}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Inactivos</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.categories}</p>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">Categorías</p>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Estado:</label>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{[
|
||||||
|
{ value: 'all', label: 'Todos' },
|
||||||
|
{ value: 'active', label: 'Activos' },
|
||||||
|
{ value: 'inactive', label: 'Inactivos' }
|
||||||
|
].map(filter => (
|
||||||
|
<button
|
||||||
|
key={filter.value}
|
||||||
|
onClick={() => setFilterStatus(filter.value as any)}
|
||||||
|
className={`px-3 py-1 text-sm rounded-full transition-colors ${
|
||||||
|
filterStatus === filter.value
|
||||||
|
? 'bg-[var(--color-primary)] text-white'
|
||||||
|
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Suppliers List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{getFilteredSuppliers().map((supplier) => (
|
||||||
|
<Card key={supplier.id} className="p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start space-x-4 flex-1">
|
||||||
|
{/* Status Icon */}
|
||||||
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center mt-1 ${
|
||||||
|
supplier.status === 'active'
|
||||||
|
? 'bg-[var(--color-success)]/10'
|
||||||
|
: 'bg-[var(--bg-secondary)] border border-[var(--border-secondary)]'
|
||||||
|
}`}>
|
||||||
|
<Truck className={`w-4 h-4 ${
|
||||||
|
supplier.status === 'active'
|
||||||
|
? 'text-[var(--color-success)]'
|
||||||
|
: 'text-[var(--text-tertiary)]'
|
||||||
|
}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Supplier Info */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{supplier.name}</h4>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Badge variant={supplier.status === 'active' ? 'green' : 'gray'}>
|
||||||
|
{supplier.status === 'active' ? 'Activo' : 'Inactivo'}
|
||||||
|
</Badge>
|
||||||
|
{supplier.categories.slice(0, 2).map((cat, idx) => (
|
||||||
|
<Badge key={idx} variant="blue">{cat}</Badge>
|
||||||
|
))}
|
||||||
|
{supplier.categories.length > 2 && (
|
||||||
|
<Badge variant="gray">+{supplier.categories.length - 2}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-6 text-sm text-[var(--text-secondary)] mb-2">
|
||||||
|
<div>
|
||||||
|
<span className="text-[var(--text-tertiary)]">Contacto: </span>
|
||||||
|
<span className="font-medium text-[var(--text-primary)]">{supplier.contact_person}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-[var(--text-tertiary)]">Entrega: </span>
|
||||||
|
<span className="font-medium text-[var(--text-primary)]">
|
||||||
|
{supplier.delivery_days.slice(0, 2).join(', ')}
|
||||||
|
{supplier.delivery_days.length > 2 && ` +${supplier.delivery_days.length - 2}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-[var(--text-tertiary)]">Pago: </span>
|
||||||
|
<span className="font-medium text-[var(--text-primary)]">{supplier.payment_terms}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{supplier.min_order_amount && (
|
||||||
|
<div>
|
||||||
|
<span className="text-[var(--text-tertiary)]">Mín: </span>
|
||||||
|
<span className="font-medium text-[var(--text-primary)]">${supplier.min_order_amount}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Phone className="w-3 h-3" />
|
||||||
|
<span>{supplier.phone}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Mail className="w-3 h-3" />
|
||||||
|
<span>{supplier.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<MapPin className="w-3 h-3" />
|
||||||
|
<span>{supplier.address}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{supplier.notes && (
|
||||||
|
<div className="mt-3 p-2 bg-[var(--bg-secondary)] rounded text-sm">
|
||||||
|
<span className="text-[var(--text-tertiary)]">Notas: </span>
|
||||||
|
<span className="text-[var(--text-primary)]">{supplier.notes}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex gap-2 ml-4 mt-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setEditingSupplier(supplier)}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4 mr-1" />
|
||||||
|
Editar
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => toggleSupplierStatus(supplier.id)}
|
||||||
|
className={supplier.status === 'active'
|
||||||
|
? 'text-[var(--color-warning)] border-[var(--color-warning)] hover:bg-[var(--color-warning)]/10'
|
||||||
|
: 'text-[var(--color-success)] border-[var(--color-success)] hover:bg-[var(--color-success)]/10'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{supplier.status === 'active' ? 'Pausar' : 'Activar'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleDeleteSupplier(supplier.id)}
|
||||||
|
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{getFilteredSuppliers().length === 0 && (
|
||||||
|
<Card className="p-8 text-center">
|
||||||
|
<Truck className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
|
||||||
|
<p className="text-[var(--text-secondary)] mb-4">No hay proveedores en esta categoría</p>
|
||||||
|
<Button onClick={handleAddSupplier}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Agregar Primer Proveedor
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Edit Modal */}
|
||||||
|
{editingSupplier && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<Card className="p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
|
||||||
|
{isAddingNew ? 'Agregar Proveedor' : 'Editar Proveedor'}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<SupplierForm
|
||||||
|
supplier={editingSupplier}
|
||||||
|
onSave={handleSaveSupplier}
|
||||||
|
onCancel={() => {
|
||||||
|
setEditingSupplier(null);
|
||||||
|
setIsAddingNew(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Information */}
|
||||||
|
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
||||||
|
<h4 className="font-medium text-[var(--color-info)] mb-2">
|
||||||
|
🚚 Gestión de Proveedores:
|
||||||
|
</h4>
|
||||||
|
<ul className="text-sm text-[var(--color-info)] space-y-1">
|
||||||
|
<li>• <strong>Este paso es opcional</strong> - puedes configurar proveedores más tarde</li>
|
||||||
|
<li>• Define categorías de productos para facilitar la búsqueda</li>
|
||||||
|
<li>• Establece días de entrega y términos de pago</li>
|
||||||
|
<li>• Configura montos mínimos de pedido para optimizar compras</li>
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Component for editing suppliers
|
||||||
|
interface SupplierFormProps {
|
||||||
|
supplier: Supplier;
|
||||||
|
onSave: (supplier: Supplier) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SupplierForm: React.FC<SupplierFormProps> = ({ supplier, onSave, onCancel }) => {
|
||||||
|
const [formData, setFormData] = useState(supplier);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
alert('El nombre es requerido');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.contact_person.trim()) {
|
||||||
|
alert('El contacto es requerido');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSave(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCategory = (category: string) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
categories: prev.categories.includes(category)
|
||||||
|
? prev.categories.filter(c => c !== category)
|
||||||
|
: [...prev.categories, category]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDeliveryDay = (day: string) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
delivery_days: prev.delivery_days.includes(day)
|
||||||
|
? prev.delivery_days.filter(d => d !== day)
|
||||||
|
: [...prev.delivery_days, day]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Nombre de la Empresa *
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
placeholder="Molinos del Sur"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Persona de Contacto *
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={formData.contact_person}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, contact_person: e.target.value }))}
|
||||||
|
placeholder="Juan Pérez"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Teléfono
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, phone: e.target.value }))}
|
||||||
|
placeholder="+1 555-0123"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
|
||||||
|
placeholder="ventas@proveedor.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Dirección
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={formData.address}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, address: e.target.value }))}
|
||||||
|
placeholder="Av. Industrial 123, Zona Sur"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Categorías de Productos
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||||
|
{commonCategories.map(category => (
|
||||||
|
<label key={category} className="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.categories.includes(category)}
|
||||||
|
onChange={() => toggleCategory(category)}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-[var(--text-primary)]">{category}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Términos de Pago
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.payment_terms}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, payment_terms: e.target.value }))}
|
||||||
|
className="w-full p-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
>
|
||||||
|
<option value="Inmediato">Inmediato</option>
|
||||||
|
<option value="15 días">15 días</option>
|
||||||
|
<option value="30 días">30 días</option>
|
||||||
|
<option value="45 días">45 días</option>
|
||||||
|
<option value="60 días">60 días</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Pedido Mínimo
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={formData.min_order_amount || ''}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, min_order_amount: Number(e.target.value) }))}
|
||||||
|
placeholder="200"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Días de Entrega
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{daysOfWeek.map(day => (
|
||||||
|
<label key={day} className="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.delivery_days.includes(day)}
|
||||||
|
onChange={() => toggleDeliveryDay(day)}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-[var(--text-primary)]">{day}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Notas
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.notes || ''}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, notes: e.target.value }))}
|
||||||
|
placeholder="Información adicional sobre el proveedor..."
|
||||||
|
className="w-full p-2 border border-[var(--border-secondary)] rounded resize-none focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3 pt-4 border-t">
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
Guardar Proveedor
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
40
frontend/src/components/shared/LoadingSpinner.tsx
Normal file
40
frontend/src/components/shared/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface LoadingSpinnerProps {
|
||||||
|
overlay?: boolean;
|
||||||
|
text?: string;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
||||||
|
overlay = false,
|
||||||
|
text,
|
||||||
|
size = 'md'
|
||||||
|
}) => {
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'w-4 h-4',
|
||||||
|
md: 'w-8 h-8',
|
||||||
|
lg: 'w-12 h-12'
|
||||||
|
};
|
||||||
|
|
||||||
|
const spinner = (
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<div className={`animate-spin rounded-full border-4 border-[var(--border-secondary)] border-t-[var(--color-primary)] ${sizeClasses[size]}`}></div>
|
||||||
|
{text && (
|
||||||
|
<p className="mt-4 text-[var(--text-secondary)] text-sm">{text}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (overlay) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-[var(--bg-primary)] rounded-lg p-6">
|
||||||
|
{spinner}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spinner;
|
||||||
|
};
|
||||||
18
frontend/src/hooks/useAuth.ts
Normal file
18
frontend/src/hooks/useAuth.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Mock auth hook for testing
|
||||||
|
export const useAuth = () => {
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: 'user_123',
|
||||||
|
tenant_id: 'tenant_456',
|
||||||
|
email: 'user@example.com',
|
||||||
|
name: 'Usuario Demo'
|
||||||
|
},
|
||||||
|
isAuthenticated: true,
|
||||||
|
login: async (credentials: any) => {
|
||||||
|
console.log('Mock login:', credentials);
|
||||||
|
},
|
||||||
|
logout: () => {
|
||||||
|
console.log('Mock logout');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
161
frontend/src/pages/app/onboarding/OnboardingPage.tsx
Normal file
161
frontend/src/pages/app/onboarding/OnboardingPage.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { OnboardingWizard, OnboardingStep } from '../../../components/domain/onboarding/OnboardingWizard';
|
||||||
|
import { onboardingApiService } from '../../../services/api/onboarding.service';
|
||||||
|
import { useAuth } from '../../../hooks/useAuth';
|
||||||
|
import { LoadingSpinner } from '../../../components/shared/LoadingSpinner';
|
||||||
|
|
||||||
|
// Step Components
|
||||||
|
import { BakerySetupStep } from '../../../components/domain/onboarding/steps/BakerySetupStep';
|
||||||
|
import { DataProcessingStep } from '../../../components/domain/onboarding/steps/DataProcessingStep';
|
||||||
|
import { ReviewStep } from '../../../components/domain/onboarding/steps/ReviewStep';
|
||||||
|
import { InventorySetupStep } from '../../../components/domain/onboarding/steps/InventorySetupStep';
|
||||||
|
import { SuppliersStep } from '../../../components/domain/onboarding/steps/SuppliersStep';
|
||||||
|
import { MLTrainingStep } from '../../../components/domain/onboarding/steps/MLTrainingStep';
|
||||||
|
import { CompletionStep } from '../../../components/domain/onboarding/steps/CompletionStep';
|
||||||
|
|
||||||
|
const OnboardingPage: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [globalData, setGlobalData] = useState<any>({});
|
||||||
|
|
||||||
|
// Define the 8 onboarding steps (simplified by merging data upload + analysis)
|
||||||
|
const steps: OnboardingStep[] = [
|
||||||
|
{
|
||||||
|
id: 'setup',
|
||||||
|
title: '🏢 Setup',
|
||||||
|
description: 'Configuración básica de tu panadería y creación del tenant',
|
||||||
|
component: BakerySetupStep,
|
||||||
|
isRequired: true,
|
||||||
|
validation: (data) => {
|
||||||
|
if (!data.bakery?.name) return 'El nombre de la panadería es requerido';
|
||||||
|
if (!data.bakery?.type) return 'El tipo de panadería es requerido';
|
||||||
|
if (!data.bakery?.location) return 'La ubicación es requerida';
|
||||||
|
// Tenant creation will happen automatically when validation passes
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'data-processing',
|
||||||
|
title: '📊 Historial de Ventas',
|
||||||
|
description: 'Sube tus datos de ventas para obtener insights personalizados',
|
||||||
|
component: DataProcessingStep,
|
||||||
|
isRequired: true,
|
||||||
|
validation: (data) => {
|
||||||
|
if (!data.files?.salesData) return 'Debes cargar el archivo de datos de ventas';
|
||||||
|
if (data.processingStage !== 'completed') return 'El procesamiento debe completarse antes de continuar';
|
||||||
|
if (!data.processingResults?.is_valid) return 'Los datos deben ser válidos para continuar';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'review',
|
||||||
|
title: '📋 Revisión',
|
||||||
|
description: 'Revisión de productos detectados por IA y resultados',
|
||||||
|
component: ReviewStep,
|
||||||
|
isRequired: true,
|
||||||
|
validation: (data) => {
|
||||||
|
if (!data.reviewCompleted) return 'Debes revisar y aprobar los productos detectados';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'inventory',
|
||||||
|
title: '⚙️ Inventario',
|
||||||
|
description: 'Configuración de inventario (stock, fechas de vencimiento)',
|
||||||
|
component: InventorySetupStep,
|
||||||
|
isRequired: true,
|
||||||
|
validation: (data) => {
|
||||||
|
if (!data.inventoryConfigured) return 'Debes configurar el inventario básico';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'suppliers',
|
||||||
|
title: '🏪 Proveedores',
|
||||||
|
description: 'Configuración de proveedores y asociaciones',
|
||||||
|
component: SuppliersStep,
|
||||||
|
isRequired: false,
|
||||||
|
validation: () => null // Optional step
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ml-training',
|
||||||
|
title: '🎯 Inteligencia',
|
||||||
|
description: 'Creación de tu asistente inteligente personalizado',
|
||||||
|
component: MLTrainingStep,
|
||||||
|
isRequired: true,
|
||||||
|
validation: (data) => {
|
||||||
|
if (data.trainingStatus !== 'completed') return 'El entrenamiento del modelo debe completarse';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'completion',
|
||||||
|
title: '🎉 Listo',
|
||||||
|
description: 'Finalización y preparación para usar la plataforma',
|
||||||
|
component: CompletionStep,
|
||||||
|
isRequired: true,
|
||||||
|
validation: () => null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleComplete = async (allData: any) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Mark onboarding as complete in the backend
|
||||||
|
if (user?.tenant_id) {
|
||||||
|
await onboardingApiService.completeOnboarding(user.tenant_id, {
|
||||||
|
completedAt: new Date().toISOString(),
|
||||||
|
data: allData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to dashboard
|
||||||
|
navigate('/app/dashboard', {
|
||||||
|
state: {
|
||||||
|
message: '¡Felicidades! Tu panadería ha sido configurada exitosamente.',
|
||||||
|
type: 'success'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error completing onboarding:', error);
|
||||||
|
// Still navigate to dashboard but show warning
|
||||||
|
navigate('/app/dashboard', {
|
||||||
|
state: {
|
||||||
|
message: 'Configuración completada. Algunos ajustes finales pueden estar pendientes.',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExit = () => {
|
||||||
|
const confirmExit = window.confirm(
|
||||||
|
'¿Estás seguro de que quieres salir del proceso de configuración? Tu progreso se guardará automáticamente.'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmExit) {
|
||||||
|
navigate('/app/dashboard');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingSpinner overlay text="Completando configuración..." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[var(--bg-primary)]">
|
||||||
|
<OnboardingWizard
|
||||||
|
steps={steps}
|
||||||
|
onComplete={handleComplete}
|
||||||
|
onExit={handleExit}
|
||||||
|
className="py-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OnboardingPage;
|
||||||
@@ -1,451 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { BarChart3, TrendingUp, Target, AlertCircle, CheckCircle, Eye, Download } from 'lucide-react';
|
|
||||||
import { Button, Card, Badge } from '../../../../components/ui';
|
|
||||||
import { PageHeader } from '../../../../components/layout';
|
|
||||||
|
|
||||||
const OnboardingAnalysisPage: React.FC = () => {
|
|
||||||
const [selectedPeriod, setSelectedPeriod] = useState('30days');
|
|
||||||
|
|
||||||
const analysisData = {
|
|
||||||
onboardingScore: 87,
|
|
||||||
completionRate: 92,
|
|
||||||
averageTime: '4.2 días',
|
|
||||||
stepsCompleted: 15,
|
|
||||||
totalSteps: 16,
|
|
||||||
dataQuality: 94
|
|
||||||
};
|
|
||||||
|
|
||||||
const stepProgress = [
|
|
||||||
{ step: 'Información Básica', completed: true, quality: 95, timeSpent: '25 min' },
|
|
||||||
{ step: 'Configuración de Menú', completed: true, quality: 88, timeSpent: '1.2 horas' },
|
|
||||||
{ step: 'Datos de Inventario', completed: true, quality: 92, timeSpent: '45 min' },
|
|
||||||
{ step: 'Configuración de Horarios', completed: true, quality: 100, timeSpent: '15 min' },
|
|
||||||
{ step: 'Integración de Pagos', completed: true, quality: 85, timeSpent: '30 min' },
|
|
||||||
{ step: 'Carga de Productos', completed: true, quality: 90, timeSpent: '2.1 horas' },
|
|
||||||
{ step: 'Configuración de Personal', completed: true, quality: 87, timeSpent: '40 min' },
|
|
||||||
{ step: 'Pruebas del Sistema', completed: false, quality: 0, timeSpent: '-' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const insights = [
|
|
||||||
{
|
|
||||||
type: 'success',
|
|
||||||
title: 'Excelente Progreso',
|
|
||||||
description: 'Has completado el 94% del proceso de configuración inicial',
|
|
||||||
recommendation: 'Solo faltan las pruebas finales del sistema para completar tu configuración',
|
|
||||||
impact: 'high'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'info',
|
|
||||||
title: 'Calidad de Datos Alta',
|
|
||||||
description: 'Tus datos tienen una calidad promedio del 94%',
|
|
||||||
recommendation: 'Considera revisar la configuración de pagos para mejorar la puntuación',
|
|
||||||
impact: 'medium'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'warning',
|
|
||||||
title: 'Paso Pendiente',
|
|
||||||
description: 'Las pruebas del sistema están pendientes',
|
|
||||||
recommendation: 'Programa las pruebas para validar la configuración completa',
|
|
||||||
impact: 'high'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const dataAnalysis = [
|
|
||||||
{
|
|
||||||
category: 'Información del Negocio',
|
|
||||||
completeness: 100,
|
|
||||||
accuracy: 95,
|
|
||||||
items: 12,
|
|
||||||
issues: 0,
|
|
||||||
details: 'Toda la información básica está completa y verificada'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: 'Menú y Productos',
|
|
||||||
completeness: 85,
|
|
||||||
accuracy: 88,
|
|
||||||
items: 45,
|
|
||||||
issues: 3,
|
|
||||||
details: '3 productos sin precios definidos'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: 'Inventario Inicial',
|
|
||||||
completeness: 92,
|
|
||||||
accuracy: 90,
|
|
||||||
items: 28,
|
|
||||||
issues: 2,
|
|
||||||
details: '2 ingredientes sin stock mínimo definido'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: 'Configuración Operativa',
|
|
||||||
completeness: 100,
|
|
||||||
accuracy: 100,
|
|
||||||
items: 8,
|
|
||||||
issues: 0,
|
|
||||||
details: 'Horarios y políticas completamente configuradas'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const benchmarkComparison = {
|
|
||||||
industry: {
|
|
||||||
onboardingScore: 74,
|
|
||||||
completionRate: 78,
|
|
||||||
averageTime: '6.8 días'
|
|
||||||
},
|
|
||||||
yourData: {
|
|
||||||
onboardingScore: 87,
|
|
||||||
completionRate: 92,
|
|
||||||
averageTime: '4.2 días'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const recommendations = [
|
|
||||||
{
|
|
||||||
priority: 'high',
|
|
||||||
title: 'Completar Pruebas del Sistema',
|
|
||||||
description: 'Realizar pruebas integrales para validar toda la configuración',
|
|
||||||
estimatedTime: '30 minutos',
|
|
||||||
impact: 'Garantiza funcionamiento óptimo del sistema'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 'medium',
|
|
||||||
title: 'Revisar Precios de Productos',
|
|
||||||
description: 'Definir precios para los 3 productos pendientes',
|
|
||||||
estimatedTime: '15 minutos',
|
|
||||||
impact: 'Permitirá generar ventas de todos los productos'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 'medium',
|
|
||||||
title: 'Configurar Stocks Mínimos',
|
|
||||||
description: 'Establecer niveles mínimos para 2 ingredientes',
|
|
||||||
estimatedTime: '10 minutos',
|
|
||||||
impact: 'Mejorará el control de inventario automático'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 'low',
|
|
||||||
title: 'Optimizar Configuración de Pagos',
|
|
||||||
description: 'Revisar métodos de pago y comisiones',
|
|
||||||
estimatedTime: '20 minutos',
|
|
||||||
impact: 'Puede reducir costos de transacción'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const getInsightIcon = (type: string) => {
|
|
||||||
const iconProps = { className: "w-5 h-5" };
|
|
||||||
switch (type) {
|
|
||||||
case 'success': return <CheckCircle {...iconProps} className="w-5 h-5 text-[var(--color-success)]" />;
|
|
||||||
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
|
|
||||||
case 'info': return <Target {...iconProps} className="w-5 h-5 text-[var(--color-info)]" />;
|
|
||||||
default: return <AlertCircle {...iconProps} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getInsightColor = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'success': return 'bg-green-50 border-green-200';
|
|
||||||
case 'warning': return 'bg-yellow-50 border-yellow-200';
|
|
||||||
case 'info': return 'bg-[var(--color-info)]/5 border-[var(--color-info)]/20';
|
|
||||||
default: return 'bg-[var(--bg-secondary)] border-[var(--border-primary)]';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPriorityColor = (priority: string) => {
|
|
||||||
switch (priority) {
|
|
||||||
case 'high': return 'red';
|
|
||||||
case 'medium': return 'yellow';
|
|
||||||
case 'low': return 'green';
|
|
||||||
default: return 'gray';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCompletionColor = (percentage: number) => {
|
|
||||||
if (percentage >= 95) return 'text-[var(--color-success)]';
|
|
||||||
if (percentage >= 80) return 'text-yellow-600';
|
|
||||||
return 'text-[var(--color-error)]';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
<PageHeader
|
|
||||||
title="Análisis de Configuración"
|
|
||||||
description="Análisis detallado de tu proceso de configuración y recomendaciones"
|
|
||||||
action={
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button variant="outline">
|
|
||||||
<Eye className="w-4 h-4 mr-2" />
|
|
||||||
Ver Detalles
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline">
|
|
||||||
<Download className="w-4 h-4 mr-2" />
|
|
||||||
Exportar Reporte
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Overall Score */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-6">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="relative w-24 h-24 mx-auto mb-3">
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<span className="text-2xl font-bold text-[var(--color-success)]">{analysisData.onboardingScore}</span>
|
|
||||||
</div>
|
|
||||||
<svg className="w-24 h-24 transform -rotate-90">
|
|
||||||
<circle
|
|
||||||
cx="48"
|
|
||||||
cy="48"
|
|
||||||
r="40"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="8"
|
|
||||||
fill="none"
|
|
||||||
className="text-gray-200"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx="48"
|
|
||||||
cy="48"
|
|
||||||
r="40"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="8"
|
|
||||||
fill="none"
|
|
||||||
strokeDasharray={`${analysisData.onboardingScore * 2.51} 251`}
|
|
||||||
className="text-[var(--color-success)]"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Puntuación General</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-[var(--color-info)]">{analysisData.completionRate}%</p>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Completado</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-purple-600">{analysisData.averageTime}</p>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Tiempo Promedio</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-[var(--color-primary)]">{analysisData.stepsCompleted}/{analysisData.totalSteps}</p>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Pasos Completados</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-teal-600">{analysisData.dataQuality}%</p>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Calidad de Datos</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="flex items-center justify-center mb-2">
|
|
||||||
<TrendingUp className="w-8 h-8 text-[var(--color-success)]" />
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Por encima del promedio</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Progress Analysis */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Progreso por Pasos</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{stepProgress.map((step, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
|
||||||
step.completed ? 'bg-[var(--color-success)]/10' : 'bg-[var(--bg-tertiary)]'
|
|
||||||
}`}>
|
|
||||||
{step.completed ? (
|
|
||||||
<CheckCircle className="w-5 h-5 text-[var(--color-success)]" />
|
|
||||||
) : (
|
|
||||||
<span className="text-sm font-medium text-[var(--text-tertiary)]">{index + 1}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-primary)]">{step.step}</p>
|
|
||||||
<p className="text-xs text-[var(--text-tertiary)]">Tiempo: {step.timeSpent}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<p className={`text-sm font-medium ${getCompletionColor(step.quality)}`}>
|
|
||||||
{step.quality}%
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-[var(--text-tertiary)]">Calidad</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Comparación con la Industria</h3>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Puntuación de Configuración</span>
|
|
||||||
<div className="text-right">
|
|
||||||
<span className="text-lg font-bold text-[var(--color-success)]">{benchmarkComparison.yourData.onboardingScore}</span>
|
|
||||||
<span className="text-sm text-[var(--text-tertiary)] ml-2">vs {benchmarkComparison.industry.onboardingScore}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
|
|
||||||
<div className="bg-green-600 h-2 rounded-full" style={{ width: `${(benchmarkComparison.yourData.onboardingScore / 100) * 100}%` }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Tasa de Completado</span>
|
|
||||||
<div className="text-right">
|
|
||||||
<span className="text-lg font-bold text-[var(--color-info)]">{benchmarkComparison.yourData.completionRate}%</span>
|
|
||||||
<span className="text-sm text-[var(--text-tertiary)] ml-2">vs {benchmarkComparison.industry.completionRate}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
|
|
||||||
<div className="bg-blue-600 h-2 rounded-full" style={{ width: `${benchmarkComparison.yourData.completionRate}%` }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Tiempo de Configuración</span>
|
|
||||||
<div className="text-right">
|
|
||||||
<span className="text-lg font-bold text-purple-600">{benchmarkComparison.yourData.averageTime}</span>
|
|
||||||
<span className="text-sm text-[var(--text-tertiary)] ml-2">vs {benchmarkComparison.industry.averageTime}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-green-50 p-3 rounded-lg">
|
|
||||||
<p className="text-sm text-[var(--color-success)]">38% más rápido que el promedio de la industria</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Insights */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Insights y Recomendaciones</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{insights.map((insight, index) => (
|
|
||||||
<div key={index} className={`p-4 rounded-lg border ${getInsightColor(insight.type)}`}>
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
{getInsightIcon(insight.type)}
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-1">{insight.title}</h4>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mb-2">{insight.description}</p>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
Recomendación: {insight.recommendation}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Badge variant={insight.impact === 'high' ? 'red' : insight.impact === 'medium' ? 'yellow' : 'green'}>
|
|
||||||
{insight.impact === 'high' ? 'Alto' : insight.impact === 'medium' ? 'Medio' : 'Bajo'} Impacto
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Data Analysis */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Análisis de Calidad de Datos</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{dataAnalysis.map((category, index) => (
|
|
||||||
<div key={index} className="border rounded-lg p-4">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h4 className="font-medium text-[var(--text-primary)]">{category.category}</h4>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<BarChart3 className="w-4 h-4 text-[var(--text-tertiary)]" />
|
|
||||||
<span className="text-sm text-[var(--text-tertiary)]">{category.items} elementos</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between text-sm mb-1">
|
|
||||||
<span>Completitud</span>
|
|
||||||
<span className={getCompletionColor(category.completeness)}>{category.completeness}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
||||||
style={{ width: `${category.completeness}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between text-sm mb-1">
|
|
||||||
<span>Precisión</span>
|
|
||||||
<span className={getCompletionColor(category.accuracy)}>{category.accuracy}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="bg-green-600 h-2 rounded-full transition-all duration-300"
|
|
||||||
style={{ width: `${category.accuracy}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{category.issues > 0 && (
|
|
||||||
<div className="flex items-center text-sm text-[var(--color-error)]">
|
|
||||||
<AlertCircle className="w-4 h-4 mr-1" />
|
|
||||||
<span>{category.issues} problema{category.issues > 1 ? 's' : ''} detectado{category.issues > 1 ? 's' : ''}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p className="text-xs text-[var(--text-secondary)]">{category.details}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Action Items */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Elementos de Acción</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{recommendations.map((rec, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
|
|
||||||
<div className="flex items-start space-x-3 flex-1">
|
|
||||||
<Badge variant={getPriorityColor(rec.priority)}>
|
|
||||||
{rec.priority === 'high' ? 'Alta' : rec.priority === 'medium' ? 'Media' : 'Baja'}
|
|
||||||
</Badge>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-1">{rec.title}</h4>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mb-1">{rec.description}</p>
|
|
||||||
<div className="flex items-center space-x-4 text-xs text-[var(--text-tertiary)]">
|
|
||||||
<span>Tiempo estimado: {rec.estimatedTime}</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>Impacto: {rec.impact}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button size="sm">
|
|
||||||
Completar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => window.location.href = '/app/onboarding/upload'}
|
|
||||||
>
|
|
||||||
← Volver a Carga de Datos
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => window.location.href = '/app/onboarding/review'}
|
|
||||||
>
|
|
||||||
Continuar a Revisión Final →
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OnboardingAnalysisPage;
|
|
||||||
@@ -1,435 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { BarChart3, TrendingUp, Target, AlertCircle, CheckCircle, Eye, Download } from 'lucide-react';
|
|
||||||
import { Button, Card, Badge } from '../../../../components/ui';
|
|
||||||
import { PageHeader } from '../../../../components/layout';
|
|
||||||
|
|
||||||
const OnboardingAnalysisPage: React.FC = () => {
|
|
||||||
const [selectedPeriod, setSelectedPeriod] = useState('30days');
|
|
||||||
|
|
||||||
const analysisData = {
|
|
||||||
onboardingScore: 87,
|
|
||||||
completionRate: 92,
|
|
||||||
averageTime: '4.2 días',
|
|
||||||
stepsCompleted: 15,
|
|
||||||
totalSteps: 16,
|
|
||||||
dataQuality: 94
|
|
||||||
};
|
|
||||||
|
|
||||||
const stepProgress = [
|
|
||||||
{ step: 'Información Básica', completed: true, quality: 95, timeSpent: '25 min' },
|
|
||||||
{ step: 'Configuración de Menú', completed: true, quality: 88, timeSpent: '1.2 horas' },
|
|
||||||
{ step: 'Datos de Inventario', completed: true, quality: 92, timeSpent: '45 min' },
|
|
||||||
{ step: 'Configuración de Horarios', completed: true, quality: 100, timeSpent: '15 min' },
|
|
||||||
{ step: 'Integración de Pagos', completed: true, quality: 85, timeSpent: '30 min' },
|
|
||||||
{ step: 'Carga de Productos', completed: true, quality: 90, timeSpent: '2.1 horas' },
|
|
||||||
{ step: 'Configuración de Personal', completed: true, quality: 87, timeSpent: '40 min' },
|
|
||||||
{ step: 'Pruebas del Sistema', completed: false, quality: 0, timeSpent: '-' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const insights = [
|
|
||||||
{
|
|
||||||
type: 'success',
|
|
||||||
title: 'Excelente Progreso',
|
|
||||||
description: 'Has completado el 94% del proceso de configuración inicial',
|
|
||||||
recommendation: 'Solo faltan las pruebas finales del sistema para completar tu configuración',
|
|
||||||
impact: 'high'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'info',
|
|
||||||
title: 'Calidad de Datos Alta',
|
|
||||||
description: 'Tus datos tienen una calidad promedio del 94%',
|
|
||||||
recommendation: 'Considera revisar la configuración de pagos para mejorar la puntuación',
|
|
||||||
impact: 'medium'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'warning',
|
|
||||||
title: 'Paso Pendiente',
|
|
||||||
description: 'Las pruebas del sistema están pendientes',
|
|
||||||
recommendation: 'Programa las pruebas para validar la configuración completa',
|
|
||||||
impact: 'high'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const dataAnalysis = [
|
|
||||||
{
|
|
||||||
category: 'Información del Negocio',
|
|
||||||
completeness: 100,
|
|
||||||
accuracy: 95,
|
|
||||||
items: 12,
|
|
||||||
issues: 0,
|
|
||||||
details: 'Toda la información básica está completa y verificada'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: 'Menú y Productos',
|
|
||||||
completeness: 85,
|
|
||||||
accuracy: 88,
|
|
||||||
items: 45,
|
|
||||||
issues: 3,
|
|
||||||
details: '3 productos sin precios definidos'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: 'Inventario Inicial',
|
|
||||||
completeness: 92,
|
|
||||||
accuracy: 90,
|
|
||||||
items: 28,
|
|
||||||
issues: 2,
|
|
||||||
details: '2 ingredientes sin stock mínimo definido'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: 'Configuración Operativa',
|
|
||||||
completeness: 100,
|
|
||||||
accuracy: 100,
|
|
||||||
items: 8,
|
|
||||||
issues: 0,
|
|
||||||
details: 'Horarios y políticas completamente configuradas'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const benchmarkComparison = {
|
|
||||||
industry: {
|
|
||||||
onboardingScore: 74,
|
|
||||||
completionRate: 78,
|
|
||||||
averageTime: '6.8 días'
|
|
||||||
},
|
|
||||||
yourData: {
|
|
||||||
onboardingScore: 87,
|
|
||||||
completionRate: 92,
|
|
||||||
averageTime: '4.2 días'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const recommendations = [
|
|
||||||
{
|
|
||||||
priority: 'high',
|
|
||||||
title: 'Completar Pruebas del Sistema',
|
|
||||||
description: 'Realizar pruebas integrales para validar toda la configuración',
|
|
||||||
estimatedTime: '30 minutos',
|
|
||||||
impact: 'Garantiza funcionamiento óptimo del sistema'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 'medium',
|
|
||||||
title: 'Revisar Precios de Productos',
|
|
||||||
description: 'Definir precios para los 3 productos pendientes',
|
|
||||||
estimatedTime: '15 minutos',
|
|
||||||
impact: 'Permitirá generar ventas de todos los productos'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 'medium',
|
|
||||||
title: 'Configurar Stocks Mínimos',
|
|
||||||
description: 'Establecer niveles mínimos para 2 ingredientes',
|
|
||||||
estimatedTime: '10 minutos',
|
|
||||||
impact: 'Mejorará el control de inventario automático'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 'low',
|
|
||||||
title: 'Optimizar Configuración de Pagos',
|
|
||||||
description: 'Revisar métodos de pago y comisiones',
|
|
||||||
estimatedTime: '20 minutos',
|
|
||||||
impact: 'Puede reducir costos de transacción'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const getInsightIcon = (type: string) => {
|
|
||||||
const iconProps = { className: "w-5 h-5" };
|
|
||||||
switch (type) {
|
|
||||||
case 'success': return <CheckCircle {...iconProps} className="w-5 h-5 text-green-600" />;
|
|
||||||
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
|
|
||||||
case 'info': return <Target {...iconProps} className="w-5 h-5 text-blue-600" />;
|
|
||||||
default: return <AlertCircle {...iconProps} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getInsightColor = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'success': return 'bg-green-50 border-green-200';
|
|
||||||
case 'warning': return 'bg-yellow-50 border-yellow-200';
|
|
||||||
case 'info': return 'bg-blue-50 border-blue-200';
|
|
||||||
default: return 'bg-gray-50 border-gray-200';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPriorityColor = (priority: string) => {
|
|
||||||
switch (priority) {
|
|
||||||
case 'high': return 'red';
|
|
||||||
case 'medium': return 'yellow';
|
|
||||||
case 'low': return 'green';
|
|
||||||
default: return 'gray';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCompletionColor = (percentage: number) => {
|
|
||||||
if (percentage >= 95) return 'text-green-600';
|
|
||||||
if (percentage >= 80) return 'text-yellow-600';
|
|
||||||
return 'text-red-600';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
<PageHeader
|
|
||||||
title="Análisis de Configuración"
|
|
||||||
description="Análisis detallado de tu proceso de configuración y recomendaciones"
|
|
||||||
action={
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button variant="outline">
|
|
||||||
<Eye className="w-4 h-4 mr-2" />
|
|
||||||
Ver Detalles
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline">
|
|
||||||
<Download className="w-4 h-4 mr-2" />
|
|
||||||
Exportar Reporte
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Overall Score */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-6">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="relative w-24 h-24 mx-auto mb-3">
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<span className="text-2xl font-bold text-green-600">{analysisData.onboardingScore}</span>
|
|
||||||
</div>
|
|
||||||
<svg className="w-24 h-24 transform -rotate-90">
|
|
||||||
<circle
|
|
||||||
cx="48"
|
|
||||||
cy="48"
|
|
||||||
r="40"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="8"
|
|
||||||
fill="none"
|
|
||||||
className="text-gray-200"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx="48"
|
|
||||||
cy="48"
|
|
||||||
r="40"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="8"
|
|
||||||
fill="none"
|
|
||||||
strokeDasharray={`${analysisData.onboardingScore * 2.51} 251`}
|
|
||||||
className="text-green-600"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-gray-700">Puntuación General</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-blue-600">{analysisData.completionRate}%</p>
|
|
||||||
<p className="text-sm font-medium text-gray-700">Completado</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-purple-600">{analysisData.averageTime}</p>
|
|
||||||
<p className="text-sm font-medium text-gray-700">Tiempo Promedio</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-orange-600">{analysisData.stepsCompleted}/{analysisData.totalSteps}</p>
|
|
||||||
<p className="text-sm font-medium text-gray-700">Pasos Completados</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-teal-600">{analysisData.dataQuality}%</p>
|
|
||||||
<p className="text-sm font-medium text-gray-700">Calidad de Datos</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="flex items-center justify-center mb-2">
|
|
||||||
<TrendingUp className="w-8 h-8 text-green-600" />
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-gray-700">Por encima del promedio</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Progress Analysis */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Progreso por Pasos</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{stepProgress.map((step, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
|
||||||
step.completed ? 'bg-green-100' : 'bg-gray-100'
|
|
||||||
}`}>
|
|
||||||
{step.completed ? (
|
|
||||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
|
||||||
) : (
|
|
||||||
<span className="text-sm font-medium text-gray-500">{index + 1}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-900">{step.step}</p>
|
|
||||||
<p className="text-xs text-gray-500">Tiempo: {step.timeSpent}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<p className={`text-sm font-medium ${getCompletionColor(step.quality)}`}>
|
|
||||||
{step.quality}%
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">Calidad</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Comparación con la Industria</h3>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="text-sm font-medium text-gray-700">Puntuación de Configuración</span>
|
|
||||||
<div className="text-right">
|
|
||||||
<span className="text-lg font-bold text-green-600">{benchmarkComparison.yourData.onboardingScore}</span>
|
|
||||||
<span className="text-sm text-gray-500 ml-2">vs {benchmarkComparison.industry.onboardingScore}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
||||||
<div className="bg-green-600 h-2 rounded-full" style={{ width: `${(benchmarkComparison.yourData.onboardingScore / 100) * 100}%` }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="text-sm font-medium text-gray-700">Tasa de Completado</span>
|
|
||||||
<div className="text-right">
|
|
||||||
<span className="text-lg font-bold text-blue-600">{benchmarkComparison.yourData.completionRate}%</span>
|
|
||||||
<span className="text-sm text-gray-500 ml-2">vs {benchmarkComparison.industry.completionRate}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
||||||
<div className="bg-blue-600 h-2 rounded-full" style={{ width: `${benchmarkComparison.yourData.completionRate}%` }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="text-sm font-medium text-gray-700">Tiempo de Configuración</span>
|
|
||||||
<div className="text-right">
|
|
||||||
<span className="text-lg font-bold text-purple-600">{benchmarkComparison.yourData.averageTime}</span>
|
|
||||||
<span className="text-sm text-gray-500 ml-2">vs {benchmarkComparison.industry.averageTime}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-green-50 p-3 rounded-lg">
|
|
||||||
<p className="text-sm text-green-700">38% más rápido que el promedio de la industria</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Insights */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Insights y Recomendaciones</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{insights.map((insight, index) => (
|
|
||||||
<div key={index} className={`p-4 rounded-lg border ${getInsightColor(insight.type)}`}>
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
{getInsightIcon(insight.type)}
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="text-sm font-medium text-gray-900 mb-1">{insight.title}</h4>
|
|
||||||
<p className="text-sm text-gray-700 mb-2">{insight.description}</p>
|
|
||||||
<p className="text-sm font-medium text-gray-900">
|
|
||||||
Recomendación: {insight.recommendation}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Badge variant={insight.impact === 'high' ? 'red' : insight.impact === 'medium' ? 'yellow' : 'green'}>
|
|
||||||
{insight.impact === 'high' ? 'Alto' : insight.impact === 'medium' ? 'Medio' : 'Bajo'} Impacto
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Data Analysis */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Análisis de Calidad de Datos</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{dataAnalysis.map((category, index) => (
|
|
||||||
<div key={index} className="border rounded-lg p-4">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h4 className="font-medium text-gray-900">{category.category}</h4>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<BarChart3 className="w-4 h-4 text-gray-500" />
|
|
||||||
<span className="text-sm text-gray-500">{category.items} elementos</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between text-sm mb-1">
|
|
||||||
<span>Completitud</span>
|
|
||||||
<span className={getCompletionColor(category.completeness)}>{category.completeness}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
||||||
style={{ width: `${category.completeness}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between text-sm mb-1">
|
|
||||||
<span>Precisión</span>
|
|
||||||
<span className={getCompletionColor(category.accuracy)}>{category.accuracy}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="bg-green-600 h-2 rounded-full transition-all duration-300"
|
|
||||||
style={{ width: `${category.accuracy}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{category.issues > 0 && (
|
|
||||||
<div className="flex items-center text-sm text-red-600">
|
|
||||||
<AlertCircle className="w-4 h-4 mr-1" />
|
|
||||||
<span>{category.issues} problema{category.issues > 1 ? 's' : ''} detectado{category.issues > 1 ? 's' : ''}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p className="text-xs text-gray-600">{category.details}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Action Items */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Elementos de Acción</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{recommendations.map((rec, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
|
|
||||||
<div className="flex items-start space-x-3 flex-1">
|
|
||||||
<Badge variant={getPriorityColor(rec.priority)}>
|
|
||||||
{rec.priority === 'high' ? 'Alta' : rec.priority === 'medium' ? 'Media' : 'Baja'}
|
|
||||||
</Badge>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-gray-900 mb-1">{rec.title}</h4>
|
|
||||||
<p className="text-sm text-gray-600 mb-1">{rec.description}</p>
|
|
||||||
<div className="flex items-center space-x-4 text-xs text-gray-500">
|
|
||||||
<span>Tiempo estimado: {rec.estimatedTime}</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>Impacto: {rec.impact}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button size="sm">
|
|
||||||
Completar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OnboardingAnalysisPage;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as OnboardingAnalysisPage } from './OnboardingAnalysisPage';
|
|
||||||
@@ -1,610 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { CheckCircle, AlertCircle, Edit2, Eye, Star, ArrowRight, Clock, Users, Zap } from 'lucide-react';
|
|
||||||
import { Button, Card, Badge } from '../../../../components/ui';
|
|
||||||
import { PageHeader } from '../../../../components/layout';
|
|
||||||
|
|
||||||
const OnboardingReviewPage: React.FC = () => {
|
|
||||||
const [activeSection, setActiveSection] = useState<string>('overview');
|
|
||||||
|
|
||||||
const completionData = {
|
|
||||||
overallProgress: 95,
|
|
||||||
totalSteps: 8,
|
|
||||||
completedSteps: 7,
|
|
||||||
remainingSteps: 1,
|
|
||||||
estimatedTimeRemaining: '15 minutos',
|
|
||||||
overallScore: 87
|
|
||||||
};
|
|
||||||
|
|
||||||
const sectionReview = [
|
|
||||||
{
|
|
||||||
id: 'business-info',
|
|
||||||
title: 'Información del Negocio',
|
|
||||||
status: 'completed',
|
|
||||||
score: 98,
|
|
||||||
items: [
|
|
||||||
{ field: 'Nombre de la panadería', value: 'Panadería Artesanal El Buen Pan', status: 'complete' },
|
|
||||||
{ field: 'Dirección', value: 'Av. Principal 123, Centro', status: 'complete' },
|
|
||||||
{ field: 'Teléfono', value: '+1 234 567 8900', status: 'complete' },
|
|
||||||
{ field: 'Email', value: 'info@elbuenpan.com', status: 'complete' },
|
|
||||||
{ field: 'Tipo de negocio', value: 'Panadería y Pastelería Artesanal', status: 'complete' },
|
|
||||||
{ field: 'Horario de operación', value: 'L-V: 6:00-20:00, S-D: 7:00-18:00', status: 'complete' }
|
|
||||||
],
|
|
||||||
recommendations: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'menu-products',
|
|
||||||
title: 'Menú y Productos',
|
|
||||||
status: 'completed',
|
|
||||||
score: 85,
|
|
||||||
items: [
|
|
||||||
{ field: 'Productos de panadería', value: '24 productos configurados', status: 'complete' },
|
|
||||||
{ field: 'Productos de pastelería', value: '18 productos configurados', status: 'complete' },
|
|
||||||
{ field: 'Bebidas', value: '12 opciones disponibles', status: 'complete' },
|
|
||||||
{ field: 'Precios configurados', value: '51 de 54 productos (94%)', status: 'warning' },
|
|
||||||
{ field: 'Categorías organizadas', value: '6 categorías principales', status: 'complete' },
|
|
||||||
{ field: 'Descripciones', value: '48 de 54 productos (89%)', status: 'warning' }
|
|
||||||
],
|
|
||||||
recommendations: [
|
|
||||||
'Completar precios para 3 productos pendientes',
|
|
||||||
'Añadir descripciones para 6 productos restantes'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'inventory',
|
|
||||||
title: 'Inventario Inicial',
|
|
||||||
status: 'completed',
|
|
||||||
score: 92,
|
|
||||||
items: [
|
|
||||||
{ field: 'Materias primas', value: '45 ingredientes registrados', status: 'complete' },
|
|
||||||
{ field: 'Proveedores', value: '8 proveedores configurados', status: 'complete' },
|
|
||||||
{ field: 'Stocks iniciales', value: '43 de 45 ingredientes (96%)', status: 'warning' },
|
|
||||||
{ field: 'Puntos de reorden', value: '40 de 45 ingredientes (89%)', status: 'warning' },
|
|
||||||
{ field: 'Costos unitarios', value: 'Completado al 100%', status: 'complete' }
|
|
||||||
],
|
|
||||||
recommendations: [
|
|
||||||
'Definir stocks iniciales para 2 ingredientes',
|
|
||||||
'Establecer puntos de reorden para 5 ingredientes'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'staff-config',
|
|
||||||
title: 'Configuración de Personal',
|
|
||||||
status: 'completed',
|
|
||||||
score: 90,
|
|
||||||
items: [
|
|
||||||
{ field: 'Empleados registrados', value: '12 miembros del equipo', status: 'complete' },
|
|
||||||
{ field: 'Roles asignados', value: '5 roles diferentes configurados', status: 'complete' },
|
|
||||||
{ field: 'Horarios de trabajo', value: '11 de 12 empleados (92%)', status: 'warning' },
|
|
||||||
{ field: 'Permisos del sistema', value: 'Configurado completamente', status: 'complete' },
|
|
||||||
{ field: 'Datos de contacto', value: 'Completado al 100%', status: 'complete' }
|
|
||||||
],
|
|
||||||
recommendations: [
|
|
||||||
'Completar horario para 1 empleado pendiente'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'operations',
|
|
||||||
title: 'Configuración Operativa',
|
|
||||||
status: 'completed',
|
|
||||||
score: 95,
|
|
||||||
items: [
|
|
||||||
{ field: 'Horarios de operación', value: 'Configurado completamente', status: 'complete' },
|
|
||||||
{ field: 'Métodos de pago', value: '4 métodos activos', status: 'complete' },
|
|
||||||
{ field: 'Políticas de devoluciones', value: 'Definidas y configuradas', status: 'complete' },
|
|
||||||
{ field: 'Configuración de impuestos', value: '18% IVA configurado', status: 'complete' },
|
|
||||||
{ field: 'Zonas de entrega', value: '3 zonas configuradas', status: 'complete' }
|
|
||||||
],
|
|
||||||
recommendations: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'integrations',
|
|
||||||
title: 'Integraciones',
|
|
||||||
status: 'completed',
|
|
||||||
score: 88,
|
|
||||||
items: [
|
|
||||||
{ field: 'Sistema de pagos', value: 'Stripe configurado', status: 'complete' },
|
|
||||||
{ field: 'Notificaciones por email', value: 'SMTP configurado', status: 'complete' },
|
|
||||||
{ field: 'Notificaciones SMS', value: 'Twilio configurado', status: 'complete' },
|
|
||||||
{ field: 'Backup automático', value: 'Configurado diariamente', status: 'complete' },
|
|
||||||
{ field: 'APIs externas', value: '2 de 3 configuradas', status: 'warning' }
|
|
||||||
],
|
|
||||||
recommendations: [
|
|
||||||
'Configurar API de delivery restante'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'testing',
|
|
||||||
title: 'Pruebas del Sistema',
|
|
||||||
status: 'pending',
|
|
||||||
score: 0,
|
|
||||||
items: [
|
|
||||||
{ field: 'Prueba de ventas', value: 'Pendiente', status: 'pending' },
|
|
||||||
{ field: 'Prueba de inventario', value: 'Pendiente', status: 'pending' },
|
|
||||||
{ field: 'Prueba de reportes', value: 'Pendiente', status: 'pending' },
|
|
||||||
{ field: 'Prueba de notificaciones', value: 'Pendiente', status: 'pending' },
|
|
||||||
{ field: 'Validación de integraciones', value: 'Pendiente', status: 'pending' }
|
|
||||||
],
|
|
||||||
recommendations: [
|
|
||||||
'Ejecutar todas las pruebas del sistema antes del lanzamiento'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'training',
|
|
||||||
title: 'Capacitación del Equipo',
|
|
||||||
status: 'completed',
|
|
||||||
score: 82,
|
|
||||||
items: [
|
|
||||||
{ field: 'Capacitación básica', value: '10 de 12 empleados (83%)', status: 'warning' },
|
|
||||||
{ field: 'Manual del usuario', value: 'Entregado y revisado', status: 'complete' },
|
|
||||||
{ field: 'Sesiones prácticas', value: '2 de 3 sesiones completadas', status: 'warning' },
|
|
||||||
{ field: 'Evaluaciones', value: '8 de 10 empleados aprobados', status: 'warning' },
|
|
||||||
{ field: 'Material de referencia', value: 'Disponible en el sistema', status: 'complete' }
|
|
||||||
],
|
|
||||||
recommendations: [
|
|
||||||
'Completar capacitación para 2 empleados pendientes',
|
|
||||||
'Programar tercera sesión práctica',
|
|
||||||
'Realizar evaluaciones pendientes'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const overallRecommendations = [
|
|
||||||
{
|
|
||||||
priority: 'high',
|
|
||||||
category: 'Crítico',
|
|
||||||
title: 'Completar Pruebas del Sistema',
|
|
||||||
description: 'Ejecutar todas las pruebas funcionales antes del lanzamiento',
|
|
||||||
estimatedTime: '30 minutos',
|
|
||||||
impact: 'Garantiza funcionamiento correcto del sistema'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 'medium',
|
|
||||||
category: 'Importante',
|
|
||||||
title: 'Finalizar Configuración de Productos',
|
|
||||||
description: 'Completar precios y descripciones pendientes',
|
|
||||||
estimatedTime: '20 minutos',
|
|
||||||
impact: 'Permite ventas completas de todos los productos'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 'medium',
|
|
||||||
category: 'Importante',
|
|
||||||
title: 'Completar Capacitación del Personal',
|
|
||||||
description: 'Finalizar entrenamiento para empleados pendientes',
|
|
||||||
estimatedTime: '45 minutos',
|
|
||||||
impact: 'Asegura operación eficiente desde el primer día'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 'low',
|
|
||||||
category: 'Opcional',
|
|
||||||
title: 'Optimizar Configuración de Inventario',
|
|
||||||
description: 'Definir stocks y puntos de reorden pendientes',
|
|
||||||
estimatedTime: '15 minutos',
|
|
||||||
impact: 'Mejora control automático de inventario'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const launchReadiness = {
|
|
||||||
essential: {
|
|
||||||
completed: 6,
|
|
||||||
total: 7,
|
|
||||||
percentage: 86
|
|
||||||
},
|
|
||||||
recommended: {
|
|
||||||
completed: 8,
|
|
||||||
total: 12,
|
|
||||||
percentage: 67
|
|
||||||
},
|
|
||||||
optional: {
|
|
||||||
completed: 3,
|
|
||||||
total: 6,
|
|
||||||
percentage: 50
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
|
||||||
const iconProps = { className: "w-5 h-5" };
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return <CheckCircle {...iconProps} className="w-5 h-5 text-[var(--color-success)]" />;
|
|
||||||
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
|
|
||||||
case 'pending': return <Clock {...iconProps} className="w-5 h-5 text-[var(--text-secondary)]" />;
|
|
||||||
default: return <AlertCircle {...iconProps} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'green';
|
|
||||||
case 'warning': return 'yellow';
|
|
||||||
case 'pending': return 'gray';
|
|
||||||
default: return 'red';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getItemStatusIcon = (status: string) => {
|
|
||||||
const iconProps = { className: "w-4 h-4" };
|
|
||||||
switch (status) {
|
|
||||||
case 'complete': return <CheckCircle {...iconProps} className="w-4 h-4 text-[var(--color-success)]" />;
|
|
||||||
case 'warning': return <AlertCircle {...iconProps} className="w-4 h-4 text-yellow-600" />;
|
|
||||||
case 'pending': return <Clock {...iconProps} className="w-4 h-4 text-[var(--text-secondary)]" />;
|
|
||||||
default: return <AlertCircle {...iconProps} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPriorityColor = (priority: string) => {
|
|
||||||
switch (priority) {
|
|
||||||
case 'high': return 'red';
|
|
||||||
case 'medium': return 'yellow';
|
|
||||||
case 'low': return 'green';
|
|
||||||
default: return 'gray';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getScoreColor = (score: number) => {
|
|
||||||
if (score >= 90) return 'text-[var(--color-success)]';
|
|
||||||
if (score >= 80) return 'text-yellow-600';
|
|
||||||
if (score >= 70) return 'text-[var(--color-primary)]';
|
|
||||||
return 'text-[var(--color-error)]';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
<PageHeader
|
|
||||||
title="Revisión Final de Configuración"
|
|
||||||
description="Verifica todos los aspectos de tu configuración antes del lanzamiento"
|
|
||||||
action={
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button variant="outline">
|
|
||||||
<Edit2 className="w-4 h-4 mr-2" />
|
|
||||||
Editar Configuración
|
|
||||||
</Button>
|
|
||||||
<Button>
|
|
||||||
<Zap className="w-4 h-4 mr-2" />
|
|
||||||
Lanzar Sistema
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Overall Progress */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="relative w-20 h-20 mx-auto mb-3">
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<span className={`text-2xl font-bold ${getScoreColor(completionData.overallScore)}`}>
|
|
||||||
{completionData.overallScore}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<svg className="w-20 h-20 transform -rotate-90">
|
|
||||||
<circle
|
|
||||||
cx="40"
|
|
||||||
cy="40"
|
|
||||||
r="32"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="6"
|
|
||||||
fill="none"
|
|
||||||
className="text-gray-200"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx="40"
|
|
||||||
cy="40"
|
|
||||||
r="32"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="6"
|
|
||||||
fill="none"
|
|
||||||
strokeDasharray={`${completionData.overallScore * 2.01} 201`}
|
|
||||||
className="text-[var(--color-success)]"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Puntuación General</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-[var(--color-info)]">{completionData.overallProgress}%</p>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Progreso Total</p>
|
|
||||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2 rounded-full"
|
|
||||||
style={{ width: `${completionData.overallProgress}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-purple-600">
|
|
||||||
{completionData.completedSteps}/{completionData.totalSteps}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Secciones Completadas</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-[var(--color-primary)]">{completionData.estimatedTimeRemaining}</p>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Tiempo Restante</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Navigation Tabs */}
|
|
||||||
<div className="border-b border-[var(--border-primary)]">
|
|
||||||
<nav className="-mb-px flex space-x-8">
|
|
||||||
{['overview', 'sections', 'recommendations', 'readiness'].map((tab) => (
|
|
||||||
<button
|
|
||||||
key={tab}
|
|
||||||
onClick={() => setActiveSection(tab)}
|
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
|
||||||
activeSection === tab
|
|
||||||
? 'border-blue-500 text-[var(--color-info)]'
|
|
||||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tab === 'overview' && 'Resumen General'}
|
|
||||||
{tab === 'sections' && 'Revisión por Secciones'}
|
|
||||||
{tab === 'recommendations' && 'Recomendaciones'}
|
|
||||||
{tab === 'readiness' && 'Preparación para Lanzamiento'}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content based on active section */}
|
|
||||||
{activeSection === 'overview' && (
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Estado por Secciones</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{sectionReview.map((section) => (
|
|
||||||
<div key={section.id} className="flex items-center justify-between p-3 border rounded-lg">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
{getStatusIcon(section.status)}
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-primary)]">{section.title}</p>
|
|
||||||
<p className="text-xs text-[var(--text-tertiary)]">
|
|
||||||
{section.recommendations.length > 0
|
|
||||||
? `${section.recommendations.length} recomendación${section.recommendations.length > 1 ? 'es' : ''}`
|
|
||||||
: 'Completado correctamente'
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<p className={`text-sm font-medium ${getScoreColor(section.score)}`}>
|
|
||||||
{section.score}%
|
|
||||||
</p>
|
|
||||||
<Badge variant={getStatusColor(section.status)}>
|
|
||||||
{section.status === 'completed' ? 'Completado' :
|
|
||||||
section.status === 'warning' ? 'Con observaciones' : 'Pendiente'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Próximos Pasos</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{overallRecommendations.slice(0, 3).map((rec, index) => (
|
|
||||||
<div key={index} className="flex items-start space-x-3 p-3 border rounded-lg">
|
|
||||||
<Badge variant={getPriorityColor(rec.priority)} className="mt-1">
|
|
||||||
{rec.category}
|
|
||||||
</Badge>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="text-sm font-medium text-[var(--text-primary)]">{rec.title}</h4>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mt-1">{rec.description}</p>
|
|
||||||
<div className="flex items-center space-x-4 mt-2 text-xs text-[var(--text-tertiary)]">
|
|
||||||
<span>⏱️ {rec.estimatedTime}</span>
|
|
||||||
<span>💡 {rec.impact}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ArrowRight className="w-4 h-4 text-[var(--text-tertiary)] mt-1" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 p-4 bg-[var(--color-info)]/5 rounded-lg">
|
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
|
||||||
<Star className="w-5 h-5 text-[var(--color-info)]" />
|
|
||||||
<h4 className="font-medium text-blue-900">¡Excelente progreso!</h4>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-[var(--color-info)]">
|
|
||||||
Has completado el 95% de la configuración. Solo quedan algunos detalles finales antes del lanzamiento.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeSection === 'sections' && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{sectionReview.map((section) => (
|
|
||||||
<Card key={section.id} className="p-6">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
{getStatusIcon(section.status)}
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{section.title}</h3>
|
|
||||||
<Badge variant={getStatusColor(section.status)}>
|
|
||||||
Puntuación: {section.score}%
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<Eye className="w-4 h-4 mr-2" />
|
|
||||||
Ver Detalles
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
||||||
{section.items.map((item, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{getItemStatusIcon(item.status)}
|
|
||||||
<span className="text-sm font-medium text-[var(--text-secondary)]">{item.field}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">{item.value}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{section.recommendations.length > 0 && (
|
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
|
||||||
<h4 className="text-sm font-medium text-yellow-800 mb-2">Recomendaciones:</h4>
|
|
||||||
<ul className="text-sm text-yellow-700 space-y-1">
|
|
||||||
{section.recommendations.map((rec, index) => (
|
|
||||||
<li key={index} className="flex items-start">
|
|
||||||
<span className="mr-2">•</span>
|
|
||||||
<span>{rec}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeSection === 'recommendations' && (
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Todas las Recomendaciones</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{overallRecommendations.map((rec, index) => (
|
|
||||||
<div key={index} className="flex items-start justify-between p-4 border rounded-lg">
|
|
||||||
<div className="flex items-start space-x-3 flex-1">
|
|
||||||
<Badge variant={getPriorityColor(rec.priority)}>
|
|
||||||
{rec.category}
|
|
||||||
</Badge>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-1">{rec.title}</h4>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mb-2">{rec.description}</p>
|
|
||||||
<div className="flex items-center space-x-4 text-xs text-[var(--text-tertiary)]">
|
|
||||||
<span>⏱️ Tiempo estimado: {rec.estimatedTime}</span>
|
|
||||||
<span>💡 Impacto: {rec.impact}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button size="sm" variant="outline">Más Información</Button>
|
|
||||||
<Button size="sm">Completar</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeSection === 'readiness' && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Preparación para el Lanzamiento</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
|
||||||
<div className="text-center p-4 border rounded-lg">
|
|
||||||
<div className="w-16 h-16 mx-auto mb-3 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
|
||||||
<CheckCircle className="w-8 h-8 text-[var(--color-success)]" />
|
|
||||||
</div>
|
|
||||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Elementos Esenciales</h4>
|
|
||||||
<p className="text-2xl font-bold text-[var(--color-success)] mb-1">
|
|
||||||
{launchReadiness.essential.completed}/{launchReadiness.essential.total}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)]">{launchReadiness.essential.percentage}% completado</p>
|
|
||||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
|
|
||||||
<div
|
|
||||||
className="bg-green-600 h-2 rounded-full"
|
|
||||||
style={{ width: `${launchReadiness.essential.percentage}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center p-4 border rounded-lg">
|
|
||||||
<div className="w-16 h-16 mx-auto mb-3 bg-yellow-100 rounded-full flex items-center justify-center">
|
|
||||||
<Star className="w-8 h-8 text-yellow-600" />
|
|
||||||
</div>
|
|
||||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Recomendados</h4>
|
|
||||||
<p className="text-2xl font-bold text-yellow-600 mb-1">
|
|
||||||
{launchReadiness.recommended.completed}/{launchReadiness.recommended.total}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)]">{launchReadiness.recommended.percentage}% completado</p>
|
|
||||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
|
|
||||||
<div
|
|
||||||
className="bg-yellow-600 h-2 rounded-full"
|
|
||||||
style={{ width: `${launchReadiness.recommended.percentage}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center p-4 border rounded-lg">
|
|
||||||
<div className="w-16 h-16 mx-auto mb-3 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
|
||||||
<Users className="w-8 h-8 text-[var(--color-info)]" />
|
|
||||||
</div>
|
|
||||||
<h4 className="font-medium text-[var(--text-primary)] mb-2">Opcionales</h4>
|
|
||||||
<p className="text-2xl font-bold text-[var(--color-info)] mb-1">
|
|
||||||
{launchReadiness.optional.completed}/{launchReadiness.optional.total}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)]">{launchReadiness.optional.percentage}% completado</p>
|
|
||||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2 rounded-full"
|
|
||||||
style={{ width: `${launchReadiness.optional.percentage}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-green-50 border border-green-200 rounded-lg p-6">
|
|
||||||
<div className="flex items-center space-x-3 mb-4">
|
|
||||||
<CheckCircle className="w-6 h-6 text-[var(--color-success)]" />
|
|
||||||
<h4 className="text-lg font-semibold text-green-900">¡Listo para Lanzar!</h4>
|
|
||||||
</div>
|
|
||||||
<p className="text-[var(--color-success)] mb-4">
|
|
||||||
Tu configuración está lista para el lanzamiento. Todos los elementos esenciales están completados
|
|
||||||
y el sistema está preparado para comenzar a operar.
|
|
||||||
</p>
|
|
||||||
<div className="flex space-x-3">
|
|
||||||
<Button
|
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
onClick={() => {
|
|
||||||
alert('¡Felicitaciones! El onboarding ha sido completado exitosamente. Ahora puedes comenzar a usar la plataforma.');
|
|
||||||
window.location.href = '/app/dashboard';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Zap className="w-4 h-4 mr-2" />
|
|
||||||
Lanzar Ahora
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline">
|
|
||||||
Ejecutar Pruebas Finales
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Overall Navigation - Always visible */}
|
|
||||||
<div className="flex justify-between items-center mt-8 p-4 bg-[var(--bg-secondary)] rounded-lg">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => window.location.href = '/app/onboarding/analysis'}
|
|
||||||
>
|
|
||||||
← Volver al Análisis
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-sm text-[var(--text-secondary)]">Último paso del onboarding</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
onClick={() => {
|
|
||||||
alert('¡Felicitaciones! El onboarding ha sido completado exitosamente. Ahora puedes comenzar a usar la plataforma.');
|
|
||||||
window.location.href = '/app/dashboard';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Zap className="w-4 h-4 mr-2" />
|
|
||||||
Finalizar Onboarding
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OnboardingReviewPage;
|
|
||||||
@@ -1,579 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { CheckCircle, AlertCircle, Edit2, Eye, Star, ArrowRight, Clock, Users, Zap } from 'lucide-react';
|
|
||||||
import { Button, Card, Badge } from '../../../../components/ui';
|
|
||||||
import { PageHeader } from '../../../../components/layout';
|
|
||||||
|
|
||||||
const OnboardingReviewPage: React.FC = () => {
|
|
||||||
const [activeSection, setActiveSection] = useState<string>('overview');
|
|
||||||
|
|
||||||
const completionData = {
|
|
||||||
overallProgress: 95,
|
|
||||||
totalSteps: 8,
|
|
||||||
completedSteps: 7,
|
|
||||||
remainingSteps: 1,
|
|
||||||
estimatedTimeRemaining: '15 minutos',
|
|
||||||
overallScore: 87
|
|
||||||
};
|
|
||||||
|
|
||||||
const sectionReview = [
|
|
||||||
{
|
|
||||||
id: 'business-info',
|
|
||||||
title: 'Información del Negocio',
|
|
||||||
status: 'completed',
|
|
||||||
score: 98,
|
|
||||||
items: [
|
|
||||||
{ field: 'Nombre de la panadería', value: 'Panadería Artesanal El Buen Pan', status: 'complete' },
|
|
||||||
{ field: 'Dirección', value: 'Av. Principal 123, Centro', status: 'complete' },
|
|
||||||
{ field: 'Teléfono', value: '+1 234 567 8900', status: 'complete' },
|
|
||||||
{ field: 'Email', value: 'info@elbuenpan.com', status: 'complete' },
|
|
||||||
{ field: 'Tipo de negocio', value: 'Panadería y Pastelería Artesanal', status: 'complete' },
|
|
||||||
{ field: 'Horario de operación', value: 'L-V: 6:00-20:00, S-D: 7:00-18:00', status: 'complete' }
|
|
||||||
],
|
|
||||||
recommendations: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'menu-products',
|
|
||||||
title: 'Menú y Productos',
|
|
||||||
status: 'completed',
|
|
||||||
score: 85,
|
|
||||||
items: [
|
|
||||||
{ field: 'Productos de panadería', value: '24 productos configurados', status: 'complete' },
|
|
||||||
{ field: 'Productos de pastelería', value: '18 productos configurados', status: 'complete' },
|
|
||||||
{ field: 'Bebidas', value: '12 opciones disponibles', status: 'complete' },
|
|
||||||
{ field: 'Precios configurados', value: '51 de 54 productos (94%)', status: 'warning' },
|
|
||||||
{ field: 'Categorías organizadas', value: '6 categorías principales', status: 'complete' },
|
|
||||||
{ field: 'Descripciones', value: '48 de 54 productos (89%)', status: 'warning' }
|
|
||||||
],
|
|
||||||
recommendations: [
|
|
||||||
'Completar precios para 3 productos pendientes',
|
|
||||||
'Añadir descripciones para 6 productos restantes'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'inventory',
|
|
||||||
title: 'Inventario Inicial',
|
|
||||||
status: 'completed',
|
|
||||||
score: 92,
|
|
||||||
items: [
|
|
||||||
{ field: 'Materias primas', value: '45 ingredientes registrados', status: 'complete' },
|
|
||||||
{ field: 'Proveedores', value: '8 proveedores configurados', status: 'complete' },
|
|
||||||
{ field: 'Stocks iniciales', value: '43 de 45 ingredientes (96%)', status: 'warning' },
|
|
||||||
{ field: 'Puntos de reorden', value: '40 de 45 ingredientes (89%)', status: 'warning' },
|
|
||||||
{ field: 'Costos unitarios', value: 'Completado al 100%', status: 'complete' }
|
|
||||||
],
|
|
||||||
recommendations: [
|
|
||||||
'Definir stocks iniciales para 2 ingredientes',
|
|
||||||
'Establecer puntos de reorden para 5 ingredientes'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'staff-config',
|
|
||||||
title: 'Configuración de Personal',
|
|
||||||
status: 'completed',
|
|
||||||
score: 90,
|
|
||||||
items: [
|
|
||||||
{ field: 'Empleados registrados', value: '12 miembros del equipo', status: 'complete' },
|
|
||||||
{ field: 'Roles asignados', value: '5 roles diferentes configurados', status: 'complete' },
|
|
||||||
{ field: 'Horarios de trabajo', value: '11 de 12 empleados (92%)', status: 'warning' },
|
|
||||||
{ field: 'Permisos del sistema', value: 'Configurado completamente', status: 'complete' },
|
|
||||||
{ field: 'Datos de contacto', value: 'Completado al 100%', status: 'complete' }
|
|
||||||
],
|
|
||||||
recommendations: [
|
|
||||||
'Completar horario para 1 empleado pendiente'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'operations',
|
|
||||||
title: 'Configuración Operativa',
|
|
||||||
status: 'completed',
|
|
||||||
score: 95,
|
|
||||||
items: [
|
|
||||||
{ field: 'Horarios de operación', value: 'Configurado completamente', status: 'complete' },
|
|
||||||
{ field: 'Métodos de pago', value: '4 métodos activos', status: 'complete' },
|
|
||||||
{ field: 'Políticas de devoluciones', value: 'Definidas y configuradas', status: 'complete' },
|
|
||||||
{ field: 'Configuración de impuestos', value: '18% IVA configurado', status: 'complete' },
|
|
||||||
{ field: 'Zonas de entrega', value: '3 zonas configuradas', status: 'complete' }
|
|
||||||
],
|
|
||||||
recommendations: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'integrations',
|
|
||||||
title: 'Integraciones',
|
|
||||||
status: 'completed',
|
|
||||||
score: 88,
|
|
||||||
items: [
|
|
||||||
{ field: 'Sistema de pagos', value: 'Stripe configurado', status: 'complete' },
|
|
||||||
{ field: 'Notificaciones por email', value: 'SMTP configurado', status: 'complete' },
|
|
||||||
{ field: 'Notificaciones SMS', value: 'Twilio configurado', status: 'complete' },
|
|
||||||
{ field: 'Backup automático', value: 'Configurado diariamente', status: 'complete' },
|
|
||||||
{ field: 'APIs externas', value: '2 de 3 configuradas', status: 'warning' }
|
|
||||||
],
|
|
||||||
recommendations: [
|
|
||||||
'Configurar API de delivery restante'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'testing',
|
|
||||||
title: 'Pruebas del Sistema',
|
|
||||||
status: 'pending',
|
|
||||||
score: 0,
|
|
||||||
items: [
|
|
||||||
{ field: 'Prueba de ventas', value: 'Pendiente', status: 'pending' },
|
|
||||||
{ field: 'Prueba de inventario', value: 'Pendiente', status: 'pending' },
|
|
||||||
{ field: 'Prueba de reportes', value: 'Pendiente', status: 'pending' },
|
|
||||||
{ field: 'Prueba de notificaciones', value: 'Pendiente', status: 'pending' },
|
|
||||||
{ field: 'Validación de integraciones', value: 'Pendiente', status: 'pending' }
|
|
||||||
],
|
|
||||||
recommendations: [
|
|
||||||
'Ejecutar todas las pruebas del sistema antes del lanzamiento'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'training',
|
|
||||||
title: 'Capacitación del Equipo',
|
|
||||||
status: 'completed',
|
|
||||||
score: 82,
|
|
||||||
items: [
|
|
||||||
{ field: 'Capacitación básica', value: '10 de 12 empleados (83%)', status: 'warning' },
|
|
||||||
{ field: 'Manual del usuario', value: 'Entregado y revisado', status: 'complete' },
|
|
||||||
{ field: 'Sesiones prácticas', value: '2 de 3 sesiones completadas', status: 'warning' },
|
|
||||||
{ field: 'Evaluaciones', value: '8 de 10 empleados aprobados', status: 'warning' },
|
|
||||||
{ field: 'Material de referencia', value: 'Disponible en el sistema', status: 'complete' }
|
|
||||||
],
|
|
||||||
recommendations: [
|
|
||||||
'Completar capacitación para 2 empleados pendientes',
|
|
||||||
'Programar tercera sesión práctica',
|
|
||||||
'Realizar evaluaciones pendientes'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const overallRecommendations = [
|
|
||||||
{
|
|
||||||
priority: 'high',
|
|
||||||
category: 'Crítico',
|
|
||||||
title: 'Completar Pruebas del Sistema',
|
|
||||||
description: 'Ejecutar todas las pruebas funcionales antes del lanzamiento',
|
|
||||||
estimatedTime: '30 minutos',
|
|
||||||
impact: 'Garantiza funcionamiento correcto del sistema'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 'medium',
|
|
||||||
category: 'Importante',
|
|
||||||
title: 'Finalizar Configuración de Productos',
|
|
||||||
description: 'Completar precios y descripciones pendientes',
|
|
||||||
estimatedTime: '20 minutos',
|
|
||||||
impact: 'Permite ventas completas de todos los productos'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 'medium',
|
|
||||||
category: 'Importante',
|
|
||||||
title: 'Completar Capacitación del Personal',
|
|
||||||
description: 'Finalizar entrenamiento para empleados pendientes',
|
|
||||||
estimatedTime: '45 minutos',
|
|
||||||
impact: 'Asegura operación eficiente desde el primer día'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 'low',
|
|
||||||
category: 'Opcional',
|
|
||||||
title: 'Optimizar Configuración de Inventario',
|
|
||||||
description: 'Definir stocks y puntos de reorden pendientes',
|
|
||||||
estimatedTime: '15 minutos',
|
|
||||||
impact: 'Mejora control automático de inventario'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const launchReadiness = {
|
|
||||||
essential: {
|
|
||||||
completed: 6,
|
|
||||||
total: 7,
|
|
||||||
percentage: 86
|
|
||||||
},
|
|
||||||
recommended: {
|
|
||||||
completed: 8,
|
|
||||||
total: 12,
|
|
||||||
percentage: 67
|
|
||||||
},
|
|
||||||
optional: {
|
|
||||||
completed: 3,
|
|
||||||
total: 6,
|
|
||||||
percentage: 50
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
|
||||||
const iconProps = { className: "w-5 h-5" };
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return <CheckCircle {...iconProps} className="w-5 h-5 text-green-600" />;
|
|
||||||
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
|
|
||||||
case 'pending': return <Clock {...iconProps} className="w-5 h-5 text-gray-600" />;
|
|
||||||
default: return <AlertCircle {...iconProps} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'green';
|
|
||||||
case 'warning': return 'yellow';
|
|
||||||
case 'pending': return 'gray';
|
|
||||||
default: return 'red';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getItemStatusIcon = (status: string) => {
|
|
||||||
const iconProps = { className: "w-4 h-4" };
|
|
||||||
switch (status) {
|
|
||||||
case 'complete': return <CheckCircle {...iconProps} className="w-4 h-4 text-green-600" />;
|
|
||||||
case 'warning': return <AlertCircle {...iconProps} className="w-4 h-4 text-yellow-600" />;
|
|
||||||
case 'pending': return <Clock {...iconProps} className="w-4 h-4 text-gray-600" />;
|
|
||||||
default: return <AlertCircle {...iconProps} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPriorityColor = (priority: string) => {
|
|
||||||
switch (priority) {
|
|
||||||
case 'high': return 'red';
|
|
||||||
case 'medium': return 'yellow';
|
|
||||||
case 'low': return 'green';
|
|
||||||
default: return 'gray';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getScoreColor = (score: number) => {
|
|
||||||
if (score >= 90) return 'text-green-600';
|
|
||||||
if (score >= 80) return 'text-yellow-600';
|
|
||||||
if (score >= 70) return 'text-orange-600';
|
|
||||||
return 'text-red-600';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
<PageHeader
|
|
||||||
title="Revisión Final de Configuración"
|
|
||||||
description="Verifica todos los aspectos de tu configuración antes del lanzamiento"
|
|
||||||
action={
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button variant="outline">
|
|
||||||
<Edit2 className="w-4 h-4 mr-2" />
|
|
||||||
Editar Configuración
|
|
||||||
</Button>
|
|
||||||
<Button>
|
|
||||||
<Zap className="w-4 h-4 mr-2" />
|
|
||||||
Lanzar Sistema
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Overall Progress */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="relative w-20 h-20 mx-auto mb-3">
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<span className={`text-2xl font-bold ${getScoreColor(completionData.overallScore)}`}>
|
|
||||||
{completionData.overallScore}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<svg className="w-20 h-20 transform -rotate-90">
|
|
||||||
<circle
|
|
||||||
cx="40"
|
|
||||||
cy="40"
|
|
||||||
r="32"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="6"
|
|
||||||
fill="none"
|
|
||||||
className="text-gray-200"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx="40"
|
|
||||||
cy="40"
|
|
||||||
r="32"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="6"
|
|
||||||
fill="none"
|
|
||||||
strokeDasharray={`${completionData.overallScore * 2.01} 201`}
|
|
||||||
className="text-green-600"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-gray-700">Puntuación General</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-blue-600">{completionData.overallProgress}%</p>
|
|
||||||
<p className="text-sm font-medium text-gray-700">Progreso Total</p>
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2 rounded-full"
|
|
||||||
style={{ width: `${completionData.overallProgress}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-purple-600">
|
|
||||||
{completionData.completedSteps}/{completionData.totalSteps}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium text-gray-700">Secciones Completadas</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-orange-600">{completionData.estimatedTimeRemaining}</p>
|
|
||||||
<p className="text-sm font-medium text-gray-700">Tiempo Restante</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Navigation Tabs */}
|
|
||||||
<div className="border-b border-gray-200">
|
|
||||||
<nav className="-mb-px flex space-x-8">
|
|
||||||
{['overview', 'sections', 'recommendations', 'readiness'].map((tab) => (
|
|
||||||
<button
|
|
||||||
key={tab}
|
|
||||||
onClick={() => setActiveSection(tab)}
|
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
|
||||||
activeSection === tab
|
|
||||||
? 'border-blue-500 text-blue-600'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tab === 'overview' && 'Resumen General'}
|
|
||||||
{tab === 'sections' && 'Revisión por Secciones'}
|
|
||||||
{tab === 'recommendations' && 'Recomendaciones'}
|
|
||||||
{tab === 'readiness' && 'Preparación para Lanzamiento'}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content based on active section */}
|
|
||||||
{activeSection === 'overview' && (
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Estado por Secciones</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{sectionReview.map((section) => (
|
|
||||||
<div key={section.id} className="flex items-center justify-between p-3 border rounded-lg">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
{getStatusIcon(section.status)}
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-900">{section.title}</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
{section.recommendations.length > 0
|
|
||||||
? `${section.recommendations.length} recomendación${section.recommendations.length > 1 ? 'es' : ''}`
|
|
||||||
: 'Completado correctamente'
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<p className={`text-sm font-medium ${getScoreColor(section.score)}`}>
|
|
||||||
{section.score}%
|
|
||||||
</p>
|
|
||||||
<Badge variant={getStatusColor(section.status)}>
|
|
||||||
{section.status === 'completed' ? 'Completado' :
|
|
||||||
section.status === 'warning' ? 'Con observaciones' : 'Pendiente'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Próximos Pasos</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{overallRecommendations.slice(0, 3).map((rec, index) => (
|
|
||||||
<div key={index} className="flex items-start space-x-3 p-3 border rounded-lg">
|
|
||||||
<Badge variant={getPriorityColor(rec.priority)} className="mt-1">
|
|
||||||
{rec.category}
|
|
||||||
</Badge>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="text-sm font-medium text-gray-900">{rec.title}</h4>
|
|
||||||
<p className="text-sm text-gray-600 mt-1">{rec.description}</p>
|
|
||||||
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-500">
|
|
||||||
<span>⏱️ {rec.estimatedTime}</span>
|
|
||||||
<span>💡 {rec.impact}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ArrowRight className="w-4 h-4 text-gray-400 mt-1" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
|
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
|
||||||
<Star className="w-5 h-5 text-blue-600" />
|
|
||||||
<h4 className="font-medium text-blue-900">¡Excelente progreso!</h4>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-blue-800">
|
|
||||||
Has completado el 95% de la configuración. Solo quedan algunos detalles finales antes del lanzamiento.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeSection === 'sections' && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{sectionReview.map((section) => (
|
|
||||||
<Card key={section.id} className="p-6">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
{getStatusIcon(section.status)}
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">{section.title}</h3>
|
|
||||||
<Badge variant={getStatusColor(section.status)}>
|
|
||||||
Puntuación: {section.score}%
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<Eye className="w-4 h-4 mr-2" />
|
|
||||||
Ver Detalles
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
||||||
{section.items.map((item, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{getItemStatusIcon(item.status)}
|
|
||||||
<span className="text-sm font-medium text-gray-700">{item.field}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-600">{item.value}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{section.recommendations.length > 0 && (
|
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
|
||||||
<h4 className="text-sm font-medium text-yellow-800 mb-2">Recomendaciones:</h4>
|
|
||||||
<ul className="text-sm text-yellow-700 space-y-1">
|
|
||||||
{section.recommendations.map((rec, index) => (
|
|
||||||
<li key={index} className="flex items-start">
|
|
||||||
<span className="mr-2">•</span>
|
|
||||||
<span>{rec}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeSection === 'recommendations' && (
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Todas las Recomendaciones</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{overallRecommendations.map((rec, index) => (
|
|
||||||
<div key={index} className="flex items-start justify-between p-4 border rounded-lg">
|
|
||||||
<div className="flex items-start space-x-3 flex-1">
|
|
||||||
<Badge variant={getPriorityColor(rec.priority)}>
|
|
||||||
{rec.category}
|
|
||||||
</Badge>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-gray-900 mb-1">{rec.title}</h4>
|
|
||||||
<p className="text-sm text-gray-600 mb-2">{rec.description}</p>
|
|
||||||
<div className="flex items-center space-x-4 text-xs text-gray-500">
|
|
||||||
<span>⏱️ Tiempo estimado: {rec.estimatedTime}</span>
|
|
||||||
<span>💡 Impacto: {rec.impact}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button size="sm" variant="outline">Más Información</Button>
|
|
||||||
<Button size="sm">Completar</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeSection === 'readiness' && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Preparación para el Lanzamiento</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
|
||||||
<div className="text-center p-4 border rounded-lg">
|
|
||||||
<div className="w-16 h-16 mx-auto mb-3 bg-green-100 rounded-full flex items-center justify-center">
|
|
||||||
<CheckCircle className="w-8 h-8 text-green-600" />
|
|
||||||
</div>
|
|
||||||
<h4 className="font-medium text-gray-900 mb-2">Elementos Esenciales</h4>
|
|
||||||
<p className="text-2xl font-bold text-green-600 mb-1">
|
|
||||||
{launchReadiness.essential.completed}/{launchReadiness.essential.total}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600">{launchReadiness.essential.percentage}% completado</p>
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
|
||||||
<div
|
|
||||||
className="bg-green-600 h-2 rounded-full"
|
|
||||||
style={{ width: `${launchReadiness.essential.percentage}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center p-4 border rounded-lg">
|
|
||||||
<div className="w-16 h-16 mx-auto mb-3 bg-yellow-100 rounded-full flex items-center justify-center">
|
|
||||||
<Star className="w-8 h-8 text-yellow-600" />
|
|
||||||
</div>
|
|
||||||
<h4 className="font-medium text-gray-900 mb-2">Recomendados</h4>
|
|
||||||
<p className="text-2xl font-bold text-yellow-600 mb-1">
|
|
||||||
{launchReadiness.recommended.completed}/{launchReadiness.recommended.total}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600">{launchReadiness.recommended.percentage}% completado</p>
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
|
||||||
<div
|
|
||||||
className="bg-yellow-600 h-2 rounded-full"
|
|
||||||
style={{ width: `${launchReadiness.recommended.percentage}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center p-4 border rounded-lg">
|
|
||||||
<div className="w-16 h-16 mx-auto mb-3 bg-blue-100 rounded-full flex items-center justify-center">
|
|
||||||
<Users className="w-8 h-8 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
<h4 className="font-medium text-gray-900 mb-2">Opcionales</h4>
|
|
||||||
<p className="text-2xl font-bold text-blue-600 mb-1">
|
|
||||||
{launchReadiness.optional.completed}/{launchReadiness.optional.total}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600">{launchReadiness.optional.percentage}% completado</p>
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2 rounded-full"
|
|
||||||
style={{ width: `${launchReadiness.optional.percentage}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-green-50 border border-green-200 rounded-lg p-6">
|
|
||||||
<div className="flex items-center space-x-3 mb-4">
|
|
||||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
|
||||||
<h4 className="text-lg font-semibold text-green-900">¡Listo para Lanzar!</h4>
|
|
||||||
</div>
|
|
||||||
<p className="text-green-800 mb-4">
|
|
||||||
Tu configuración está lista para el lanzamiento. Todos los elementos esenciales están completados
|
|
||||||
y el sistema está preparado para comenzar a operar.
|
|
||||||
</p>
|
|
||||||
<div className="flex space-x-3">
|
|
||||||
<Button className="bg-green-600 hover:bg-green-700">
|
|
||||||
<Zap className="w-4 h-4 mr-2" />
|
|
||||||
Lanzar Ahora
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline">
|
|
||||||
Ejecutar Pruebas Finales
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OnboardingReviewPage;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as OnboardingReviewPage } from './OnboardingReviewPage';
|
|
||||||
@@ -1,906 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import { ChevronRight, ChevronLeft, Check, Store, Upload, Brain } from 'lucide-react';
|
|
||||||
import { Button, Card, Input } from '../../../../components/ui';
|
|
||||||
import { PageHeader } from '../../../../components/layout';
|
|
||||||
|
|
||||||
const OnboardingSetupPage: React.FC = () => {
|
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
bakery: {
|
|
||||||
name: 'Panadería Artesanal El Buen Pan',
|
|
||||||
type: 'artisan',
|
|
||||||
size: 'medium',
|
|
||||||
location: 'Av. Principal 123, Centro Histórico',
|
|
||||||
phone: '+1 234 567 8900',
|
|
||||||
email: 'info@elbuenpan.com'
|
|
||||||
},
|
|
||||||
upload: {
|
|
||||||
salesData: null,
|
|
||||||
inventoryData: null,
|
|
||||||
recipeData: null
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const [trainingState, setTrainingState] = useState({
|
|
||||||
isTraining: false,
|
|
||||||
progress: 0,
|
|
||||||
currentStep: 'Iniciando entrenamiento...',
|
|
||||||
status: 'pending', // 'pending', 'running', 'completed', 'error'
|
|
||||||
logs: [] as string[]
|
|
||||||
});
|
|
||||||
|
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Información de la Panadería',
|
|
||||||
description: 'Detalles básicos sobre tu negocio',
|
|
||||||
icon: Store,
|
|
||||||
fields: ['name', 'type', 'location', 'contact']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Carga de Datos',
|
|
||||||
description: 'Importa tus datos existentes para acelerar la configuración inicial',
|
|
||||||
icon: Upload,
|
|
||||||
fields: ['files', 'templates', 'validation']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: 'Entrenamiento IA',
|
|
||||||
description: 'Configurando tu modelo de inteligencia artificial personalizado',
|
|
||||||
icon: Brain,
|
|
||||||
fields: ['training', 'progress', 'completion']
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const bakeryTypes = [
|
|
||||||
{
|
|
||||||
value: 'artisan',
|
|
||||||
label: 'Panadería Artesanal Local',
|
|
||||||
description: 'Producción propia y tradicional en el local'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'dependent',
|
|
||||||
label: 'Panadería Dependiente',
|
|
||||||
description: 'Dependiente de un panadero central'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleInputChange = (section: string, field: string, value: any) => {
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[section]: {
|
|
||||||
...prev[section as keyof typeof prev],
|
|
||||||
[field]: value
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleArrayToggle = (section: string, field: string, value: string) => {
|
|
||||||
setFormData(prev => {
|
|
||||||
const currentArray = prev[section as keyof typeof prev][field] || [];
|
|
||||||
const newArray = currentArray.includes(value)
|
|
||||||
? currentArray.filter((item: string) => item !== value)
|
|
||||||
: [...currentArray, value];
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[section]: {
|
|
||||||
...prev[section as keyof typeof prev],
|
|
||||||
[field]: newArray
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextStep = () => {
|
|
||||||
if (currentStep < steps.length) {
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevStep = () => {
|
|
||||||
if (currentStep > 1) {
|
|
||||||
setCurrentStep(currentStep - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinish = () => {
|
|
||||||
console.log('Onboarding completed:', formData);
|
|
||||||
// Navigate to dashboard - onboarding is complete
|
|
||||||
window.location.href = '/app/dashboard';
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadTemplate = (type: 'sales' | 'inventory' | 'recipes') => {
|
|
||||||
let csvContent = '';
|
|
||||||
let fileName = '';
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'sales':
|
|
||||||
csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
|
|
||||||
2024-01-15,Pan Integral,5,2.50,12.50,Cliente A,Tienda
|
|
||||||
2024-01-15,Croissant,3,1.80,5.40,Cliente B,Online
|
|
||||||
2024-01-15,Baguette,2,3.00,6.00,Cliente C,Tienda
|
|
||||||
2024-01-16,Pan de Centeno,4,2.80,11.20,Cliente A,Tienda
|
|
||||||
2024-01-16,Empanadas,6,4.50,27.00,Cliente D,Delivery`;
|
|
||||||
fileName = 'plantilla_ventas.csv';
|
|
||||||
break;
|
|
||||||
case 'inventory':
|
|
||||||
csvContent = `nombre,categoria,unidad_medida,stock_actual,stock_minimo,stock_maximo,precio_compra,proveedor,fecha_vencimiento
|
|
||||||
Harina de Trigo,Ingrediente,kg,50,20,100,1.20,Molinos del Sur,2024-12-31
|
|
||||||
Azúcar Blanca,Ingrediente,kg,25,10,50,0.85,Dulces SA,2024-12-31
|
|
||||||
Levadura Fresca,Ingrediente,kg,5,2,10,3.50,Levaduras Pro,2024-03-15
|
|
||||||
Mantequilla,Ingrediente,kg,15,5,30,4.20,Lácteos Premium,2024-02-28
|
|
||||||
Pan Integral,Producto Final,unidad,20,10,50,0.00,Producción Propia,2024-01-20`;
|
|
||||||
fileName = 'plantilla_inventario.csv';
|
|
||||||
break;
|
|
||||||
case 'recipes':
|
|
||||||
csvContent = `nombre_receta,categoria,tiempo_preparacion,tiempo_coccion,porciones,ingrediente,cantidad,unidad
|
|
||||||
Pan Integral,Panadería,30,45,2,Harina Integral,500,g
|
|
||||||
Pan Integral,Panadería,30,45,2,Agua,325,ml
|
|
||||||
Pan Integral,Panadería,30,45,2,Sal,10,g
|
|
||||||
Pan Integral,Panadería,30,45,2,Levadura,7,g
|
|
||||||
Croissant,Bollería,120,20,12,Harina,400,g
|
|
||||||
Croissant,Bollería,120,20,12,Mantequilla,250,g
|
|
||||||
Croissant,Bollería,120,20,12,Agua,200,ml
|
|
||||||
Croissant,Bollería,120,20,12,Sal,8,g`;
|
|
||||||
fileName = 'plantilla_recetas.csv';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and download the file
|
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
||||||
const link = document.createElement('a');
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
link.setAttribute('href', url);
|
|
||||||
link.setAttribute('download', fileName);
|
|
||||||
link.style.visibility = 'hidden';
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
};
|
|
||||||
|
|
||||||
// WebSocket connection for ML training updates
|
|
||||||
const connectWebSocket = () => {
|
|
||||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wsUrl = process.env.NODE_ENV === 'production'
|
|
||||||
? 'wss://api.bakeryai.com/ws/training'
|
|
||||||
: 'ws://localhost:8000/ws/training';
|
|
||||||
|
|
||||||
wsRef.current = new WebSocket(wsUrl);
|
|
||||||
|
|
||||||
wsRef.current.onopen = () => {
|
|
||||||
console.log('WebSocket connected for ML training');
|
|
||||||
setTrainingState(prev => ({ ...prev, status: 'running' }));
|
|
||||||
};
|
|
||||||
|
|
||||||
wsRef.current.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
setTrainingState(prev => ({
|
|
||||||
...prev,
|
|
||||||
progress: data.progress || prev.progress,
|
|
||||||
currentStep: data.step || prev.currentStep,
|
|
||||||
status: data.status || prev.status,
|
|
||||||
logs: data.log ? [...prev.logs, data.log] : prev.logs
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing WebSocket message:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
wsRef.current.onclose = () => {
|
|
||||||
console.log('WebSocket connection closed');
|
|
||||||
};
|
|
||||||
|
|
||||||
wsRef.current.onerror = (error) => {
|
|
||||||
console.error('WebSocket error:', error);
|
|
||||||
setTrainingState(prev => ({ ...prev, status: 'error' }));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const startTraining = () => {
|
|
||||||
setTrainingState(prev => ({ ...prev, isTraining: true, progress: 0 }));
|
|
||||||
connectWebSocket();
|
|
||||||
|
|
||||||
// Send training configuration to server
|
|
||||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
||||||
wsRef.current.send(JSON.stringify({
|
|
||||||
action: 'start_training',
|
|
||||||
bakery_data: formData.bakery,
|
|
||||||
upload_data: formData.upload
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cleanup WebSocket on unmount
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (wsRef.current) {
|
|
||||||
wsRef.current.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isStepComplete = (stepId: number) => {
|
|
||||||
// Basic validation logic
|
|
||||||
switch (stepId) {
|
|
||||||
case 1:
|
|
||||||
return formData.bakery.name && formData.bakery.location;
|
|
||||||
case 2:
|
|
||||||
return true; // Upload step is now optional - users can skip if they want
|
|
||||||
case 3:
|
|
||||||
return trainingState.status === 'completed';
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStepContent = () => {
|
|
||||||
switch (currentStep) {
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<div className="space-y-8">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<label className="block text-lg font-semibold text-[var(--text-primary)] mb-3">
|
|
||||||
Nombre de la Panadería *
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
value={formData.bakery.name}
|
|
||||||
onChange={(e) => handleInputChange('bakery', 'name', e.target.value)}
|
|
||||||
placeholder="Ej: Panadería Artesanal El Buen Pan"
|
|
||||||
className="w-full transition-all duration-300 focus:scale-[1.02] focus:shadow-lg"
|
|
||||||
/>
|
|
||||||
{formData.bakery.name && (
|
|
||||||
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
|
||||||
<Check className="w-5 h-5 text-[var(--color-success)]" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-lg font-semibold text-[var(--text-primary)] mb-4">
|
|
||||||
Tipo de Panadería *
|
|
||||||
</label>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{bakeryTypes.map((type) => (
|
|
||||||
<label
|
|
||||||
key={type.value}
|
|
||||||
className={`
|
|
||||||
group relative flex p-6 border-2 rounded-xl cursor-pointer transition-all duration-300 hover:scale-[1.02] hover:shadow-lg
|
|
||||||
${formData.bakery.type === type.value
|
|
||||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5 shadow-lg'
|
|
||||||
: 'border-[var(--border-secondary)] bg-[var(--bg-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-tertiary)]'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start space-x-4 w-full">
|
|
||||||
<div className="relative flex-shrink-0 mt-1">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="bakeryType"
|
|
||||||
value={type.value}
|
|
||||||
checked={formData.bakery.type === type.value}
|
|
||||||
onChange={(e) => handleInputChange('bakery', 'type', e.target.value)}
|
|
||||||
className="sr-only"
|
|
||||||
/>
|
|
||||||
<div className={`
|
|
||||||
w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all duration-300
|
|
||||||
${formData.bakery.type === type.value
|
|
||||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]'
|
|
||||||
: 'border-[var(--border-tertiary)] group-hover:border-[var(--color-primary)]'
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
{formData.bakery.type === type.value && (
|
|
||||||
<div className="w-3 h-3 bg-white rounded-full"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className={`
|
|
||||||
text-lg font-semibold mb-2 transition-colors duration-300
|
|
||||||
${formData.bakery.type === type.value ? 'text-[var(--text-primary)]' : 'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]'}
|
|
||||||
`}>
|
|
||||||
{type.label}
|
|
||||||
</h3>
|
|
||||||
<p className={`text-sm transition-colors duration-300 ${
|
|
||||||
formData.bakery.type === type.value ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)] group-hover:text-[var(--text-secondary)]'
|
|
||||||
}`}>
|
|
||||||
{type.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Selection indicator */}
|
|
||||||
{formData.bakery.type === type.value && (
|
|
||||||
<div className="absolute top-4 right-4">
|
|
||||||
<Check className="w-5 h-5 text-[var(--color-primary)]" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Ubicación *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={formData.bakery.location}
|
|
||||||
onChange={(e) => handleInputChange('bakery', 'location', e.target.value)}
|
|
||||||
placeholder="Dirección completa"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Teléfono
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={formData.bakery.phone}
|
|
||||||
onChange={(e) => handleInputChange('bakery', 'phone', e.target.value)}
|
|
||||||
placeholder="+34 xxx xxx xxx"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={formData.bakery.email}
|
|
||||||
onChange={(e) => handleInputChange('bakery', 'email', e.target.value)}
|
|
||||||
placeholder="contacto@panaderia.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<div className="space-y-8">
|
|
||||||
{/* Upload Options */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className={`grid grid-cols-1 gap-4 ${
|
|
||||||
formData.bakery.type === 'dependent' ? 'md:grid-cols-2' : 'md:grid-cols-3'
|
|
||||||
}`}>
|
|
||||||
{/* Sales Data Upload */}
|
|
||||||
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-6 text-center transition-all duration-300 hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)]/5">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-success)]/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<svg className="w-6 h-6 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">Datos de Ventas</h4>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mb-3">
|
|
||||||
Historial de ventas (CSV, Excel)
|
|
||||||
</p>
|
|
||||||
<div className="text-xs text-[var(--text-tertiary)] mb-4 bg-[var(--bg-secondary)] rounded-lg p-3 text-left">
|
|
||||||
<div className="font-medium mb-1">Columnas requeridas:</div>
|
|
||||||
<div>• <strong>Fecha</strong> (date, fecha)</div>
|
|
||||||
<div>• <strong>Producto</strong> (product, producto)</div>
|
|
||||||
<div className="font-medium mt-2 mb-1">Columnas opcionales:</div>
|
|
||||||
<div>• Cantidad, Precio, Categoría, Ubicación</div>
|
|
||||||
<div className="mt-2 text-[var(--color-info)]">Formatos: CSV, Excel | Máx: 10MB</div>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept=".csv,.xlsx,.xls"
|
|
||||||
onChange={(e) => handleInputChange('upload', 'salesData', e.target.files?.[0] || null)}
|
|
||||||
className="hidden"
|
|
||||||
id="sales-upload"
|
|
||||||
/>
|
|
||||||
<label htmlFor="sales-upload" className="btn btn-outline text-sm cursor-pointer">
|
|
||||||
{formData.upload.salesData ? 'Archivo cargado ✓' : 'Seleccionar archivo'}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Inventory Data Upload */}
|
|
||||||
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-6 text-center transition-all duration-300 hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)]/5">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-warning)]/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<svg className="w-6 h-6 text-[var(--color-warning)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">Inventario</h4>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
|
||||||
Lista de productos e ingredientes
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept=".csv,.xlsx,.xls"
|
|
||||||
onChange={(e) => handleInputChange('upload', 'inventoryData', e.target.files?.[0] || null)}
|
|
||||||
className="hidden"
|
|
||||||
id="inventory-upload"
|
|
||||||
/>
|
|
||||||
<label htmlFor="inventory-upload" className="btn btn-outline text-sm cursor-pointer">
|
|
||||||
{formData.upload.inventoryData ? 'Archivo cargado ✓' : 'Seleccionar archivo'}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Recipe Data Upload - Only for artisan bakeries */}
|
|
||||||
{formData.bakery.type === 'artisan' && (
|
|
||||||
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-6 text-center transition-all duration-300 hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)]/5">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-info)]/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<svg className="w-6 h-6 text-[var(--color-info)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">Recetas</h4>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
|
||||||
Recetas y fórmulas existentes
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept=".csv,.xlsx,.xls,.pdf"
|
|
||||||
onChange={(e) => handleInputChange('upload', 'recipeData', e.target.files?.[0] || null)}
|
|
||||||
className="hidden"
|
|
||||||
id="recipe-upload"
|
|
||||||
/>
|
|
||||||
<label htmlFor="recipe-upload" className="btn btn-outline text-sm cursor-pointer">
|
|
||||||
{formData.upload.recipeData ? 'Archivo cargado ✓' : 'Seleccionar archivo'}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Template Downloads */}
|
|
||||||
<div className="space-y-4 pt-6 border-t border-[var(--border-secondary)]">
|
|
||||||
<div className="text-center">
|
|
||||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">¿Necesitas ayuda con el formato?</h4>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
|
||||||
Descarga nuestras plantillas para estructurar correctamente tus datos
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
|
||||||
<button
|
|
||||||
onClick={() => downloadTemplate('sales')}
|
|
||||||
className="flex items-center justify-center px-4 py-2 border border-[var(--color-success)] text-[var(--color-success)] rounded-lg hover:bg-[var(--color-success)]/10 transition-all duration-200"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
||||||
</svg>
|
|
||||||
Plantilla de Ventas
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => downloadTemplate('inventory')}
|
|
||||||
className="flex items-center justify-center px-4 py-2 border border-[var(--color-warning)] text-[var(--color-warning)] rounded-lg hover:bg-[var(--color-warning)]/10 transition-all duration-200"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
||||||
</svg>
|
|
||||||
Plantilla de Inventario
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Recipe template - Only for artisan bakeries */}
|
|
||||||
{formData.bakery.type === 'artisan' && (
|
|
||||||
<button
|
|
||||||
onClick={() => downloadTemplate('recipes')}
|
|
||||||
className="flex items-center justify-center px-4 py-2 border border-[var(--color-info)] text-[var(--color-info)] rounded-lg hover:bg-[var(--color-info)]/10 transition-all duration-200"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
||||||
</svg>
|
|
||||||
Plantilla de Recetas
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 3:
|
|
||||||
return (
|
|
||||||
<div className="space-y-8">
|
|
||||||
{/* Training Progress */}
|
|
||||||
{!trainingState.isTraining && trainingState.status === 'pending' && (
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-[var(--text-secondary)] mb-6">
|
|
||||||
Presiona el botón para iniciar el entrenamiento de tu modelo de IA
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
onClick={startTraining}
|
|
||||||
className="px-8 py-3 bg-[var(--color-info)] hover:bg-[var(--color-info)]/80 text-white"
|
|
||||||
>
|
|
||||||
<Brain className="w-4 h-4 mr-2" />
|
|
||||||
Iniciar Entrenamiento
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Training in Progress */}
|
|
||||||
{trainingState.isTraining && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Progress Bar */}
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
Progreso del Entrenamiento
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">
|
|
||||||
{trainingState.progress}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-4 overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="bg-gradient-to-r from-[var(--color-info)] to-[var(--color-primary)] h-full rounded-full transition-all duration-500 ease-out relative"
|
|
||||||
style={{ width: `${trainingState.progress}%` }}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-white opacity-20 rounded-full"></div>
|
|
||||||
{trainingState.progress > 0 && (
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-pulse rounded-full"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Current Step */}
|
|
||||||
<div className="flex items-center space-x-3 p-4 bg-[var(--bg-secondary)] rounded-lg border">
|
|
||||||
<div className="w-3 h-3 bg-[var(--color-info)] rounded-full animate-pulse"></div>
|
|
||||||
<span className="text-[var(--text-primary)] font-medium">
|
|
||||||
{trainingState.currentStep}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Training Status */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div className={`p-4 rounded-lg border text-center ${
|
|
||||||
trainingState.progress >= 25
|
|
||||||
? 'bg-[var(--color-success)]/10 border-[var(--color-success)]'
|
|
||||||
: 'bg-[var(--bg-secondary)] border-[var(--border-secondary)]'
|
|
||||||
}`}>
|
|
||||||
<div className={`w-8 h-8 mx-auto mb-2 rounded-full flex items-center justify-center ${
|
|
||||||
trainingState.progress >= 25 ? 'bg-[var(--color-success)]' : 'bg-[var(--bg-tertiary)]'
|
|
||||||
}`}>
|
|
||||||
{trainingState.progress >= 25 ? (
|
|
||||||
<Check className="w-4 h-4 text-white" />
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-[var(--text-tertiary)]">1</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs font-medium">Carga de Datos</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`p-4 rounded-lg border text-center ${
|
|
||||||
trainingState.progress >= 75
|
|
||||||
? 'bg-[var(--color-success)]/10 border-[var(--color-success)]'
|
|
||||||
: 'bg-[var(--bg-secondary)] border-[var(--border-secondary)]'
|
|
||||||
}`}>
|
|
||||||
<div className={`w-8 h-8 mx-auto mb-2 rounded-full flex items-center justify-center ${
|
|
||||||
trainingState.progress >= 75 ? 'bg-[var(--color-success)]' : 'bg-[var(--bg-tertiary)]'
|
|
||||||
}`}>
|
|
||||||
{trainingState.progress >= 75 ? (
|
|
||||||
<Check className="w-4 h-4 text-white" />
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-[var(--text-tertiary)]">2</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs font-medium">Entrenamiento</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`p-4 rounded-lg border text-center ${
|
|
||||||
trainingState.status === 'completed'
|
|
||||||
? 'bg-[var(--color-success)]/10 border-[var(--color-success)]'
|
|
||||||
: 'bg-[var(--bg-secondary)] border-[var(--border-secondary)]'
|
|
||||||
}`}>
|
|
||||||
<div className={`w-8 h-8 mx-auto mb-2 rounded-full flex items-center justify-center ${
|
|
||||||
trainingState.status === 'completed' ? 'bg-[var(--color-success)]' : 'bg-[var(--bg-tertiary)]'
|
|
||||||
}`}>
|
|
||||||
{trainingState.status === 'completed' ? (
|
|
||||||
<Check className="w-4 h-4 text-white" />
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-[var(--text-tertiary)]">3</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs font-medium">Validación</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Training Logs */}
|
|
||||||
{trainingState.logs.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
|
|
||||||
Log de Entrenamiento
|
|
||||||
</h4>
|
|
||||||
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 max-h-32 overflow-y-auto border">
|
|
||||||
{trainingState.logs.slice(-5).map((log, index) => (
|
|
||||||
<div key={index} className="text-xs text-[var(--text-secondary)] mb-1 font-mono">
|
|
||||||
{log}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Training Completed */}
|
|
||||||
{trainingState.status === 'completed' && (
|
|
||||||
<div className="text-center p-6 bg-[var(--color-success)]/10 rounded-xl border border-[var(--color-success)]/20">
|
|
||||||
<Check className="w-12 h-12 text-[var(--color-success)] mx-auto mb-4" />
|
|
||||||
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
|
||||||
¡Entrenamiento Completado!
|
|
||||||
</h4>
|
|
||||||
<p className="text-[var(--text-secondary)]">
|
|
||||||
Tu modelo de IA personalizado está listo para ayudarte a optimizar tu panadería
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Training Error */}
|
|
||||||
{trainingState.status === 'error' && (
|
|
||||||
<div className="text-center p-6 bg-[var(--color-error)]/10 rounded-xl border border-[var(--color-error)]/20">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-error)] rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<span className="text-white text-xl">!</span>
|
|
||||||
</div>
|
|
||||||
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
|
||||||
Error en el Entrenamiento
|
|
||||||
</h4>
|
|
||||||
<p className="text-[var(--text-secondary)] mb-4">
|
|
||||||
Ocurrió un problema durante el entrenamiento. Por favor, inténtalo de nuevo.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
onClick={startTraining}
|
|
||||||
className="px-6 py-2 bg-[var(--color-info)] hover:bg-[var(--color-info)]/80 text-white"
|
|
||||||
>
|
|
||||||
Reintentar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4 sm:p-6 max-w-5xl mx-auto">
|
|
||||||
<div className="text-center mb-8 sm:mb-12">
|
|
||||||
<div className="inline-flex items-center justify-center w-14 h-14 sm:w-16 sm:h-16 bg-[var(--color-primary)] rounded-full mb-4 sm:mb-6 shadow-lg">
|
|
||||||
<svg className="w-6 h-6 sm:w-8 sm:h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] mb-3 sm:mb-4">
|
|
||||||
Configuración Inicial
|
|
||||||
</h1>
|
|
||||||
<p className="text-base sm:text-lg text-[var(--text-secondary)] max-w-2xl mx-auto px-4">
|
|
||||||
Configura tu panadería paso a paso para comenzar a usar la plataforma de manera óptima
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
{/* Single Integrated Card */}
|
|
||||||
<Card className="shadow-lg overflow-hidden">
|
|
||||||
{/* Progress Header Inside Card */}
|
|
||||||
<div className="bg-[var(--bg-secondary)] p-4 sm:p-6 border-b border-[var(--border-secondary)]">
|
|
||||||
{/* Step Indicators - Mobile Optimized */}
|
|
||||||
<div className="mb-4 sm:mb-6">
|
|
||||||
{/* Mobile: Vertical Step List */}
|
|
||||||
<div className="block sm:hidden space-y-3">
|
|
||||||
{steps.map((step, index) => (
|
|
||||||
<div key={step.id} className="flex items-center space-x-3">
|
|
||||||
<div className={`
|
|
||||||
w-8 h-8 rounded-full flex items-center justify-center text-xs font-medium transition-all duration-300 flex-shrink-0
|
|
||||||
${step.id <= currentStep
|
|
||||||
? 'bg-[var(--color-primary)] text-white'
|
|
||||||
: 'bg-[var(--bg-quaternary)] text-[var(--text-tertiary)] border border-[var(--border-secondary)]'
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
{step.id < currentStep ? (
|
|
||||||
<Check className="w-3 h-3" />
|
|
||||||
) : step.id === currentStep ? (
|
|
||||||
<step.icon className="w-3 h-3" />
|
|
||||||
) : (
|
|
||||||
<step.icon className="w-3 h-3" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className={`text-sm font-medium ${
|
|
||||||
step.id === currentStep
|
|
||||||
? 'text-[var(--color-primary)]'
|
|
||||||
: step.id < currentStep
|
|
||||||
? 'text-[var(--color-success)]'
|
|
||||||
: 'text-[var(--text-tertiary)]'
|
|
||||||
}`}>
|
|
||||||
{step.title}
|
|
||||||
</h3>
|
|
||||||
{step.id === currentStep && (
|
|
||||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
|
||||||
{step.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Desktop: Horizontal Step Indicators */}
|
|
||||||
<div className="hidden sm:flex items-center justify-center space-x-6">
|
|
||||||
{steps.map((step, index) => (
|
|
||||||
<div key={step.id} className="flex items-center">
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<div className={`
|
|
||||||
w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-all duration-300 shadow-sm
|
|
||||||
${step.id <= currentStep
|
|
||||||
? 'bg-[var(--color-primary)] text-white'
|
|
||||||
: 'bg-[var(--bg-quaternary)] text-[var(--text-tertiary)] border border-[var(--border-secondary)]'
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
{step.id < currentStep ? (
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
) : step.id === currentStep ? (
|
|
||||||
<step.icon className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<step.icon className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className={`text-xs mt-2 text-center max-w-16 leading-tight ${
|
|
||||||
step.id === currentStep
|
|
||||||
? 'text-[var(--color-primary)] font-semibold'
|
|
||||||
: 'text-[var(--text-tertiary)]'
|
|
||||||
}`}>
|
|
||||||
{step.title}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Connection line - Desktop only */}
|
|
||||||
{index < steps.length - 1 && (
|
|
||||||
<div className={`
|
|
||||||
w-16 h-0.5 mx-3 transition-all duration-500
|
|
||||||
${step.id < currentStep ? 'bg-[var(--color-primary)]' : 'bg-[var(--border-secondary)]'}
|
|
||||||
`} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Step Info - Desktop Only (Mobile shows inline) */}
|
|
||||||
<div className="text-center hidden sm:block">
|
|
||||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-2">
|
|
||||||
Paso {currentStep}: {steps[currentStep - 1].title}
|
|
||||||
</h2>
|
|
||||||
<p className="text-[var(--text-secondary)] mb-4">
|
|
||||||
{steps[currentStep - 1].description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Step Info */}
|
|
||||||
<div className="text-center block sm:hidden mb-4">
|
|
||||||
<h2 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
|
||||||
{steps[currentStep - 1].title}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress Bar */}
|
|
||||||
<div className="relative max-w-sm sm:max-w-md mx-auto">
|
|
||||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="bg-[var(--color-primary)] h-full rounded-full transition-all duration-700 ease-out"
|
|
||||||
style={{ width: `${(currentStep / steps.length) * 100}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="text-center mt-2">
|
|
||||||
<span className="text-sm font-medium text-[var(--color-primary)]">
|
|
||||||
{Math.round((currentStep / steps.length) * 100)}% Completado
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Form Content */}
|
|
||||||
<div className="p-4 sm:p-8">
|
|
||||||
<div className="space-y-4 sm:space-y-6">
|
|
||||||
{renderStepContent()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation Footer */}
|
|
||||||
<div className="bg-[var(--bg-secondary)] px-4 sm:px-8 py-4 sm:py-6 border-t border-[var(--border-secondary)]">
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 sm:gap-0">
|
|
||||||
{/* Mobile: Full width buttons stacked */}
|
|
||||||
<div className="flex w-full sm:w-auto gap-3 sm:hidden">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={prevStep}
|
|
||||||
disabled={currentStep === 1}
|
|
||||||
className="flex-1 py-3 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-4 h-4 mr-2" />
|
|
||||||
Anterior
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{currentStep === steps.length ? (
|
|
||||||
<Button
|
|
||||||
onClick={handleFinish}
|
|
||||||
disabled={!isStepComplete(currentStep)}
|
|
||||||
className="flex-1 py-3 bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Finalizar
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
onClick={nextStep}
|
|
||||||
disabled={!isStepComplete(currentStep)}
|
|
||||||
className="flex-1 py-3 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Siguiente
|
|
||||||
<ChevronRight className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Desktop: Original layout */}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={prevStep}
|
|
||||||
disabled={currentStep === 1}
|
|
||||||
className="hidden sm:flex px-6 py-3 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-4 h-4 mr-2" />
|
|
||||||
Anterior
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Step indicators - smaller on mobile */}
|
|
||||||
<div className="flex items-center space-x-1.5 sm:space-x-2">
|
|
||||||
{steps.map((_, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`
|
|
||||||
w-1.5 h-1.5 sm:w-2 sm:h-2 rounded-full transition-all duration-300
|
|
||||||
${index + 1 === currentStep
|
|
||||||
? 'bg-[var(--color-primary)] scale-125'
|
|
||||||
: index + 1 < currentStep
|
|
||||||
? 'bg-[var(--color-success)]'
|
|
||||||
: 'bg-[var(--border-secondary)]'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentStep === steps.length ? (
|
|
||||||
<Button
|
|
||||||
onClick={handleFinish}
|
|
||||||
disabled={!isStepComplete(currentStep)}
|
|
||||||
className="hidden sm:flex px-8 py-3 bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Finalizar
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
onClick={nextStep}
|
|
||||||
disabled={!isStepComplete(currentStep)}
|
|
||||||
className="hidden sm:flex px-6 py-3 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Siguiente
|
|
||||||
<ChevronRight className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OnboardingSetupPage;
|
|
||||||
@@ -1,499 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { ChevronRight, ChevronLeft, Check, Store, Users, Settings, Zap } from 'lucide-react';
|
|
||||||
import { Button, Card, Input } from '../../../../components/ui';
|
|
||||||
import { PageHeader } from '../../../../components/layout';
|
|
||||||
|
|
||||||
const OnboardingSetupPage: React.FC = () => {
|
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
bakery: {
|
|
||||||
name: '',
|
|
||||||
type: 'traditional',
|
|
||||||
size: 'medium',
|
|
||||||
location: '',
|
|
||||||
phone: '',
|
|
||||||
email: ''
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
ownerName: '',
|
|
||||||
teamSize: '5-10',
|
|
||||||
roles: [],
|
|
||||||
experience: 'intermediate'
|
|
||||||
},
|
|
||||||
operations: {
|
|
||||||
openingHours: {
|
|
||||||
start: '07:00',
|
|
||||||
end: '20:00'
|
|
||||||
},
|
|
||||||
daysOpen: 6,
|
|
||||||
specialties: [],
|
|
||||||
dailyProduction: 'medium'
|
|
||||||
},
|
|
||||||
goals: {
|
|
||||||
primaryGoals: [],
|
|
||||||
expectedRevenue: '',
|
|
||||||
timeline: '6months'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Información de la Panadería',
|
|
||||||
description: 'Detalles básicos sobre tu negocio',
|
|
||||||
icon: Store,
|
|
||||||
fields: ['name', 'type', 'location', 'contact']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Equipo y Personal',
|
|
||||||
description: 'Información sobre tu equipo de trabajo',
|
|
||||||
icon: Users,
|
|
||||||
fields: ['owner', 'teamSize', 'roles', 'experience']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: 'Operaciones',
|
|
||||||
description: 'Horarios y especialidades de producción',
|
|
||||||
icon: Settings,
|
|
||||||
fields: ['hours', 'specialties', 'production']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: 'Objetivos',
|
|
||||||
description: 'Metas y expectativas para tu panadería',
|
|
||||||
icon: Zap,
|
|
||||||
fields: ['goals', 'revenue', 'timeline']
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const bakeryTypes = [
|
|
||||||
{ value: 'traditional', label: 'Panadería Tradicional' },
|
|
||||||
{ value: 'artisan', label: 'Panadería Artesanal' },
|
|
||||||
{ value: 'cafe', label: 'Panadería-Café' },
|
|
||||||
{ value: 'industrial', label: 'Producción Industrial' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const specialties = [
|
|
||||||
{ value: 'bread', label: 'Pan Tradicional' },
|
|
||||||
{ value: 'pastries', label: 'Bollería' },
|
|
||||||
{ value: 'cakes', label: 'Tartas y Pasteles' },
|
|
||||||
{ value: 'cookies', label: 'Galletas' },
|
|
||||||
{ value: 'savory', label: 'Productos Salados' },
|
|
||||||
{ value: 'gluten-free', label: 'Sin Gluten' },
|
|
||||||
{ value: 'vegan', label: 'Vegano' },
|
|
||||||
{ value: 'organic', label: 'Orgánico' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const businessGoals = [
|
|
||||||
{ value: 'increase-sales', label: 'Aumentar Ventas' },
|
|
||||||
{ value: 'reduce-waste', label: 'Reducir Desperdicios' },
|
|
||||||
{ value: 'improve-efficiency', label: 'Mejorar Eficiencia' },
|
|
||||||
{ value: 'expand-menu', label: 'Ampliar Menú' },
|
|
||||||
{ value: 'digital-presence', label: 'Presencia Digital' },
|
|
||||||
{ value: 'customer-loyalty', label: 'Fidelización de Clientes' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleInputChange = (section: string, field: string, value: any) => {
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[section]: {
|
|
||||||
...prev[section as keyof typeof prev],
|
|
||||||
[field]: value
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleArrayToggle = (section: string, field: string, value: string) => {
|
|
||||||
setFormData(prev => {
|
|
||||||
const currentArray = prev[section as keyof typeof prev][field] || [];
|
|
||||||
const newArray = currentArray.includes(value)
|
|
||||||
? currentArray.filter((item: string) => item !== value)
|
|
||||||
: [...currentArray, value];
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[section]: {
|
|
||||||
...prev[section as keyof typeof prev],
|
|
||||||
[field]: newArray
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextStep = () => {
|
|
||||||
if (currentStep < steps.length) {
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevStep = () => {
|
|
||||||
if (currentStep > 1) {
|
|
||||||
setCurrentStep(currentStep - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinish = () => {
|
|
||||||
console.log('Onboarding completed:', formData);
|
|
||||||
// Handle completion logic
|
|
||||||
};
|
|
||||||
|
|
||||||
const isStepComplete = (stepId: number) => {
|
|
||||||
// Basic validation logic
|
|
||||||
switch (stepId) {
|
|
||||||
case 1:
|
|
||||||
return formData.bakery.name && formData.bakery.location;
|
|
||||||
case 2:
|
|
||||||
return formData.team.ownerName;
|
|
||||||
case 3:
|
|
||||||
return formData.operations.specialties.length > 0;
|
|
||||||
case 4:
|
|
||||||
return formData.goals.primaryGoals.length > 0;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStepContent = () => {
|
|
||||||
switch (currentStep) {
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Nombre de la Panadería *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={formData.bakery.name}
|
|
||||||
onChange={(e) => handleInputChange('bakery', 'name', e.target.value)}
|
|
||||||
placeholder="Ej: Panadería San Miguel"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Tipo de Panadería
|
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
{bakeryTypes.map((type) => (
|
|
||||||
<label key={type.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="bakeryType"
|
|
||||||
value={type.value}
|
|
||||||
checked={formData.bakery.type === type.value}
|
|
||||||
onChange={(e) => handleInputChange('bakery', 'type', e.target.value)}
|
|
||||||
className="text-blue-600"
|
|
||||||
/>
|
|
||||||
<span className="text-sm">{type.label}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Ubicación *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={formData.bakery.location}
|
|
||||||
onChange={(e) => handleInputChange('bakery', 'location', e.target.value)}
|
|
||||||
placeholder="Dirección completa"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Teléfono
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={formData.bakery.phone}
|
|
||||||
onChange={(e) => handleInputChange('bakery', 'phone', e.target.value)}
|
|
||||||
placeholder="+34 xxx xxx xxx"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={formData.bakery.email}
|
|
||||||
onChange={(e) => handleInputChange('bakery', 'email', e.target.value)}
|
|
||||||
placeholder="contacto@panaderia.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Nombre del Propietario *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={formData.team.ownerName}
|
|
||||||
onChange={(e) => handleInputChange('team', 'ownerName', e.target.value)}
|
|
||||||
placeholder="Tu nombre completo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Tamaño del Equipo
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={formData.team.teamSize}
|
|
||||||
onChange={(e) => handleInputChange('team', 'teamSize', e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
|
||||||
>
|
|
||||||
<option value="1-2">Solo yo o 1-2 personas</option>
|
|
||||||
<option value="3-5">3-5 empleados</option>
|
|
||||||
<option value="5-10">5-10 empleados</option>
|
|
||||||
<option value="10+">Más de 10 empleados</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Experiencia en el Sector
|
|
||||||
</label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{[
|
|
||||||
{ value: 'beginner', label: 'Principiante (menos de 2 años)' },
|
|
||||||
{ value: 'intermediate', label: 'Intermedio (2-5 años)' },
|
|
||||||
{ value: 'experienced', label: 'Experimentado (5-10 años)' },
|
|
||||||
{ value: 'expert', label: 'Experto (más de 10 años)' }
|
|
||||||
].map((exp) => (
|
|
||||||
<label key={exp.value} className="flex items-center space-x-3">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="experience"
|
|
||||||
value={exp.value}
|
|
||||||
checked={formData.team.experience === exp.value}
|
|
||||||
onChange={(e) => handleInputChange('team', 'experience', e.target.value)}
|
|
||||||
className="text-blue-600"
|
|
||||||
/>
|
|
||||||
<span className="text-sm">{exp.label}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 3:
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Hora de Apertura
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={formData.operations.openingHours.start}
|
|
||||||
onChange={(e) => handleInputChange('operations', 'openingHours', {
|
|
||||||
...formData.operations.openingHours,
|
|
||||||
start: e.target.value
|
|
||||||
})}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Hora de Cierre
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={formData.operations.openingHours.end}
|
|
||||||
onChange={(e) => handleInputChange('operations', 'openingHours', {
|
|
||||||
...formData.operations.openingHours,
|
|
||||||
end: e.target.value
|
|
||||||
})}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Días de Operación por Semana
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={formData.operations.daysOpen}
|
|
||||||
onChange={(e) => handleInputChange('operations', 'daysOpen', parseInt(e.target.value))}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
|
||||||
>
|
|
||||||
<option value={5}>5 días</option>
|
|
||||||
<option value={6}>6 días</option>
|
|
||||||
<option value={7}>7 días</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
||||||
Especialidades *
|
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
{specialties.map((specialty) => (
|
|
||||||
<label key={specialty.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.operations.specialties.includes(specialty.value)}
|
|
||||||
onChange={() => handleArrayToggle('operations', 'specialties', specialty.value)}
|
|
||||||
className="text-blue-600 rounded"
|
|
||||||
/>
|
|
||||||
<span className="text-sm">{specialty.label}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 4:
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
||||||
Objetivos Principales *
|
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
{businessGoals.map((goal) => (
|
|
||||||
<label key={goal.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.goals.primaryGoals.includes(goal.value)}
|
|
||||||
onChange={() => handleArrayToggle('goals', 'primaryGoals', goal.value)}
|
|
||||||
className="text-blue-600 rounded"
|
|
||||||
/>
|
|
||||||
<span className="text-sm">{goal.label}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Ingresos Mensuales Esperados (opcional)
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={formData.goals.expectedRevenue}
|
|
||||||
onChange={(e) => handleInputChange('goals', 'expectedRevenue', e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
|
||||||
>
|
|
||||||
<option value="">Seleccionar rango</option>
|
|
||||||
<option value="0-5000">Menos de €5,000</option>
|
|
||||||
<option value="5000-15000">€5,000 - €15,000</option>
|
|
||||||
<option value="15000-30000">€15,000 - €30,000</option>
|
|
||||||
<option value="30000+">Más de €30,000</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Plazo para Alcanzar Objetivos
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={formData.goals.timeline}
|
|
||||||
onChange={(e) => handleInputChange('goals', 'timeline', e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
|
||||||
>
|
|
||||||
<option value="3months">3 meses</option>
|
|
||||||
<option value="6months">6 meses</option>
|
|
||||||
<option value="1year">1 año</option>
|
|
||||||
<option value="2years">2 años o más</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 max-w-4xl mx-auto">
|
|
||||||
<PageHeader
|
|
||||||
title="Configuración Inicial"
|
|
||||||
description="Configura tu panadería paso a paso para comenzar"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Progress Steps */}
|
|
||||||
<Card className="p-6 mb-8">
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
{steps.map((step, index) => (
|
|
||||||
<div key={step.id} className="flex items-center">
|
|
||||||
<div className={`flex items-center justify-center w-10 h-10 rounded-full ${
|
|
||||||
step.id === currentStep
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: step.id < currentStep || isStepComplete(step.id)
|
|
||||||
? 'bg-green-600 text-white'
|
|
||||||
: 'bg-gray-200 text-gray-600'
|
|
||||||
}`}>
|
|
||||||
{step.id < currentStep || (step.id === currentStep && isStepComplete(step.id)) ? (
|
|
||||||
<Check className="w-5 h-5" />
|
|
||||||
) : (
|
|
||||||
<step.icon className="w-5 h-5" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{index < steps.length - 1 && (
|
|
||||||
<div className={`w-full h-1 mx-4 ${
|
|
||||||
step.id < currentStep ? 'bg-green-600' : 'bg-gray-200'
|
|
||||||
}`} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
|
||||||
Paso {currentStep}: {steps[currentStep - 1].title}
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
{steps[currentStep - 1].description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Step Content */}
|
|
||||||
<Card className="p-8 mb-8">
|
|
||||||
{renderStepContent()}
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={prevStep}
|
|
||||||
disabled={currentStep === 1}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-4 h-4 mr-2" />
|
|
||||||
Anterior
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{currentStep === steps.length ? (
|
|
||||||
<Button onClick={handleFinish}>
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Finalizar Configuración
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
onClick={nextStep}
|
|
||||||
disabled={!isStepComplete(currentStep)}
|
|
||||||
>
|
|
||||||
Siguiente
|
|
||||||
<ChevronRight className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OnboardingSetupPage;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as OnboardingSetupPage } from './OnboardingSetupPage';
|
|
||||||
@@ -1,454 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Upload, File, Check, AlertCircle, Download, RefreshCw, Trash2, Eye } from 'lucide-react';
|
|
||||||
import { Button, Card, Badge } from '../../../../components/ui';
|
|
||||||
import { PageHeader } from '../../../../components/layout';
|
|
||||||
|
|
||||||
const OnboardingUploadPage: React.FC = () => {
|
|
||||||
const [dragActive, setDragActive] = useState(false);
|
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
|
||||||
|
|
||||||
const uploadedFiles = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'productos_menu.csv',
|
|
||||||
type: 'productos',
|
|
||||||
size: '45 KB',
|
|
||||||
status: 'completed',
|
|
||||||
uploadedAt: '2024-01-26 10:30:00',
|
|
||||||
records: 127,
|
|
||||||
errors: 3,
|
|
||||||
warnings: 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'inventario_inicial.xlsx',
|
|
||||||
type: 'inventario',
|
|
||||||
size: '82 KB',
|
|
||||||
status: 'completed',
|
|
||||||
uploadedAt: '2024-01-26 10:25:00',
|
|
||||||
records: 89,
|
|
||||||
errors: 0,
|
|
||||||
warnings: 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'empleados.csv',
|
|
||||||
type: 'empleados',
|
|
||||||
size: '12 KB',
|
|
||||||
status: 'processing',
|
|
||||||
uploadedAt: '2024-01-26 10:35:00',
|
|
||||||
records: 8,
|
|
||||||
errors: 0,
|
|
||||||
warnings: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: 'ventas_historicas.csv',
|
|
||||||
type: 'ventas',
|
|
||||||
size: '256 KB',
|
|
||||||
status: 'error',
|
|
||||||
uploadedAt: '2024-01-26 10:20:00',
|
|
||||||
records: 0,
|
|
||||||
errors: 1,
|
|
||||||
warnings: 0,
|
|
||||||
errorMessage: 'Formato de fecha incorrecto en la columna "fecha_venta"'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const supportedFormats = [
|
|
||||||
{
|
|
||||||
type: 'productos',
|
|
||||||
name: 'Productos y Menú',
|
|
||||||
formats: ['CSV', 'Excel'],
|
|
||||||
description: 'Lista de productos con precios, categorías y descripciones',
|
|
||||||
template: 'template_productos.csv',
|
|
||||||
requiredFields: ['nombre', 'categoria', 'precio', 'descripcion']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'inventario',
|
|
||||||
name: 'Inventario Inicial',
|
|
||||||
formats: ['CSV', 'Excel'],
|
|
||||||
description: 'Stock inicial de ingredientes y materias primas',
|
|
||||||
template: 'template_inventario.xlsx',
|
|
||||||
requiredFields: ['item', 'cantidad', 'unidad', 'costo_unitario']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'empleados',
|
|
||||||
name: 'Empleados',
|
|
||||||
formats: ['CSV'],
|
|
||||||
description: 'Información del personal y roles',
|
|
||||||
template: 'template_empleados.csv',
|
|
||||||
requiredFields: ['nombre', 'cargo', 'email', 'telefono']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'ventas',
|
|
||||||
name: 'Historial de Ventas',
|
|
||||||
formats: ['CSV'],
|
|
||||||
description: 'Datos históricos de ventas para análisis',
|
|
||||||
template: 'template_ventas.csv',
|
|
||||||
requiredFields: ['fecha', 'producto', 'cantidad', 'total']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'proveedores',
|
|
||||||
name: 'Proveedores',
|
|
||||||
formats: ['CSV', 'Excel'],
|
|
||||||
description: 'Lista de proveedores y datos de contacto',
|
|
||||||
template: 'template_proveedores.csv',
|
|
||||||
requiredFields: ['nombre', 'contacto', 'telefono', 'email']
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const uploadStats = {
|
|
||||||
totalFiles: uploadedFiles.length,
|
|
||||||
completedFiles: uploadedFiles.filter(f => f.status === 'completed').length,
|
|
||||||
totalRecords: uploadedFiles.reduce((sum, f) => sum + f.records, 0),
|
|
||||||
totalErrors: uploadedFiles.reduce((sum, f) => sum + f.errors, 0),
|
|
||||||
totalWarnings: uploadedFiles.reduce((sum, f) => sum + f.warnings, 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
|
||||||
const iconProps = { className: "w-5 h-5" };
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return <Check {...iconProps} className="w-5 h-5 text-[var(--color-success)]" />;
|
|
||||||
case 'processing': return <RefreshCw {...iconProps} className="w-5 h-5 text-[var(--color-info)] animate-spin" />;
|
|
||||||
case 'error': return <AlertCircle {...iconProps} className="w-5 h-5 text-[var(--color-error)]" />;
|
|
||||||
default: return <File {...iconProps} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'green';
|
|
||||||
case 'processing': return 'blue';
|
|
||||||
case 'error': return 'red';
|
|
||||||
default: return 'gray';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusLabel = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'Completado';
|
|
||||||
case 'processing': return 'Procesando';
|
|
||||||
case 'error': return 'Error';
|
|
||||||
default: return status;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDragActive(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragLeave = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDragActive(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDragActive(false);
|
|
||||||
const files = Array.from(e.dataTransfer.files);
|
|
||||||
handleFiles(files);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (e.target.files) {
|
|
||||||
const files = Array.from(e.target.files);
|
|
||||||
handleFiles(files);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFiles = (files: File[]) => {
|
|
||||||
console.log('Files selected:', files);
|
|
||||||
// Simulate upload progress
|
|
||||||
setIsProcessing(true);
|
|
||||||
setUploadProgress(0);
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setUploadProgress(prev => {
|
|
||||||
if (prev >= 100) {
|
|
||||||
clearInterval(interval);
|
|
||||||
setIsProcessing(false);
|
|
||||||
return 100;
|
|
||||||
}
|
|
||||||
return prev + 10;
|
|
||||||
});
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadTemplate = (template: string) => {
|
|
||||||
console.log('Downloading template:', template);
|
|
||||||
// Handle template download
|
|
||||||
};
|
|
||||||
|
|
||||||
const retryUpload = (fileId: string) => {
|
|
||||||
console.log('Retrying upload for file:', fileId);
|
|
||||||
// Handle retry logic
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteFile = (fileId: string) => {
|
|
||||||
console.log('Deleting file:', fileId);
|
|
||||||
// Handle delete logic
|
|
||||||
};
|
|
||||||
|
|
||||||
const viewDetails = (fileId: string) => {
|
|
||||||
console.log('Viewing details for file:', fileId);
|
|
||||||
// Handle view details logic
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
<PageHeader
|
|
||||||
title="Carga de Datos"
|
|
||||||
description="Importa tus datos existentes para acelerar la configuración inicial"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Upload Stats */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Archivos Subidos</p>
|
|
||||||
<p className="text-3xl font-bold text-[var(--color-info)]">{uploadStats.totalFiles}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
|
||||||
<Upload className="h-6 w-6 text-[var(--color-info)]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Completados</p>
|
|
||||||
<p className="text-3xl font-bold text-[var(--color-success)]">{uploadStats.completedFiles}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
|
||||||
<Check className="h-6 w-6 text-[var(--color-success)]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Registros</p>
|
|
||||||
<p className="text-3xl font-bold text-purple-600">{uploadStats.totalRecords}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
|
||||||
<File className="h-6 w-6 text-purple-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Errores</p>
|
|
||||||
<p className="text-3xl font-bold text-[var(--color-error)]">{uploadStats.totalErrors}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
|
|
||||||
<AlertCircle className="h-6 w-6 text-[var(--color-error)]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Advertencias</p>
|
|
||||||
<p className="text-3xl font-bold text-yellow-600">{uploadStats.totalWarnings}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-12 w-12 bg-yellow-100 rounded-full flex items-center justify-center">
|
|
||||||
<AlertCircle className="h-6 w-6 text-yellow-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Upload Area */}
|
|
||||||
<Card className="p-8">
|
|
||||||
<div
|
|
||||||
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
|
||||||
dragActive
|
|
||||||
? 'border-blue-400 bg-[var(--color-info)]/5'
|
|
||||||
: 'border-[var(--border-secondary)] hover:border-[var(--border-tertiary)]'
|
|
||||||
}`}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
>
|
|
||||||
<Upload className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
|
||||||
Arrastra archivos aquí o haz clic para seleccionar
|
|
||||||
</h3>
|
|
||||||
<p className="text-[var(--text-secondary)] mb-4">
|
|
||||||
Formatos soportados: CSV, Excel (XLSX). Tamaño máximo: 10MB por archivo
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
accept=".csv,.xlsx,.xls"
|
|
||||||
onChange={handleFileInput}
|
|
||||||
className="hidden"
|
|
||||||
id="file-upload"
|
|
||||||
/>
|
|
||||||
<label htmlFor="file-upload">
|
|
||||||
<Button className="cursor-pointer">
|
|
||||||
Seleccionar Archivos
|
|
||||||
</Button>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{isProcessing && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mb-2">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
||||||
style={{ width: `${uploadProgress}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)]">Procesando... {uploadProgress}%</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Supported Formats */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Formatos Soportados</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{supportedFormats.map((format, index) => (
|
|
||||||
<div key={index} className="border rounded-lg p-4">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h4 className="font-medium text-[var(--text-primary)]">{format.name}</h4>
|
|
||||||
<div className="flex space-x-1">
|
|
||||||
{format.formats.map((fmt, idx) => (
|
|
||||||
<Badge key={idx} variant="blue" className="text-xs">{fmt}</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mb-3">{format.description}</p>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<p className="text-xs font-medium text-[var(--text-secondary)] mb-1">Campos requeridos:</p>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{format.requiredFields.map((field, idx) => (
|
|
||||||
<span key={idx} className="px-2 py-1 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] text-xs rounded">
|
|
||||||
{field}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => downloadTemplate(format.template)}
|
|
||||||
>
|
|
||||||
<Download className="w-3 h-3 mr-2" />
|
|
||||||
Descargar Plantilla
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Uploaded Files */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Archivos Cargados</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{uploadedFiles.map((file) => (
|
|
||||||
<div key={file.id} className="flex items-center justify-between p-4 border rounded-lg">
|
|
||||||
<div className="flex items-center space-x-4 flex-1">
|
|
||||||
{getStatusIcon(file.status)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center space-x-3 mb-1">
|
|
||||||
<h4 className="text-sm font-medium text-[var(--text-primary)]">{file.name}</h4>
|
|
||||||
<Badge variant={getStatusColor(file.status)}>
|
|
||||||
{getStatusLabel(file.status)}
|
|
||||||
</Badge>
|
|
||||||
<span className="text-xs text-[var(--text-tertiary)]">{file.size}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 text-xs text-[var(--text-secondary)]">
|
|
||||||
<span>{file.records} registros</span>
|
|
||||||
{file.errors > 0 && (
|
|
||||||
<span className="text-[var(--color-error)]">{file.errors} errores</span>
|
|
||||||
)}
|
|
||||||
{file.warnings > 0 && (
|
|
||||||
<span className="text-yellow-600">{file.warnings} advertencias</span>
|
|
||||||
)}
|
|
||||||
<span>Subido: {new Date(file.uploadedAt).toLocaleString('es-ES')}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{file.status === 'error' && file.errorMessage && (
|
|
||||||
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-[var(--color-error)]">
|
|
||||||
{file.errorMessage}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button size="sm" variant="outline" onClick={() => viewDetails(file.id)}>
|
|
||||||
<Eye className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{file.status === 'error' && (
|
|
||||||
<Button size="sm" variant="outline" onClick={() => retryUpload(file.id)}>
|
|
||||||
<RefreshCw className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button size="sm" variant="outline" onClick={() => deleteFile(file.id)}>
|
|
||||||
<Trash2 className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Help Section */}
|
|
||||||
<Card className="p-6 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
|
||||||
<h3 className="text-lg font-semibold text-blue-900 mb-4">Tips para la Carga de Datos</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-[var(--color-info)]">
|
|
||||||
<ul className="space-y-2">
|
|
||||||
<li>• Usa las plantillas proporcionadas para garantizar el formato correcto</li>
|
|
||||||
<li>• Verifica que todos los campos requeridos estén completos</li>
|
|
||||||
<li>• Los archivos CSV deben usar codificación UTF-8</li>
|
|
||||||
<li>• Las fechas deben estar en formato DD/MM/YYYY</li>
|
|
||||||
</ul>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
<li>• Los precios deben usar punto (.) como separador decimal</li>
|
|
||||||
<li>• Evita caracteres especiales en los nombres de productos</li>
|
|
||||||
<li>• Mantén los nombres de archivos descriptivos</li>
|
|
||||||
<li>• Puedes cargar múltiples archivos del mismo tipo</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => window.location.href = '/app/onboarding/setup'}
|
|
||||||
>
|
|
||||||
← Volver a Configuración
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => window.location.href = '/app/onboarding/analysis'}
|
|
||||||
>
|
|
||||||
Continuar al Análisis →
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OnboardingUploadPage;
|
|
||||||
@@ -1,438 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Upload, File, Check, AlertCircle, Download, RefreshCw, Trash2, Eye } from 'lucide-react';
|
|
||||||
import { Button, Card, Badge } from '../../../../components/ui';
|
|
||||||
import { PageHeader } from '../../../../components/layout';
|
|
||||||
|
|
||||||
const OnboardingUploadPage: React.FC = () => {
|
|
||||||
const [dragActive, setDragActive] = useState(false);
|
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
|
||||||
|
|
||||||
const uploadedFiles = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'productos_menu.csv',
|
|
||||||
type: 'productos',
|
|
||||||
size: '45 KB',
|
|
||||||
status: 'completed',
|
|
||||||
uploadedAt: '2024-01-26 10:30:00',
|
|
||||||
records: 127,
|
|
||||||
errors: 3,
|
|
||||||
warnings: 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'inventario_inicial.xlsx',
|
|
||||||
type: 'inventario',
|
|
||||||
size: '82 KB',
|
|
||||||
status: 'completed',
|
|
||||||
uploadedAt: '2024-01-26 10:25:00',
|
|
||||||
records: 89,
|
|
||||||
errors: 0,
|
|
||||||
warnings: 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'empleados.csv',
|
|
||||||
type: 'empleados',
|
|
||||||
size: '12 KB',
|
|
||||||
status: 'processing',
|
|
||||||
uploadedAt: '2024-01-26 10:35:00',
|
|
||||||
records: 8,
|
|
||||||
errors: 0,
|
|
||||||
warnings: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: 'ventas_historicas.csv',
|
|
||||||
type: 'ventas',
|
|
||||||
size: '256 KB',
|
|
||||||
status: 'error',
|
|
||||||
uploadedAt: '2024-01-26 10:20:00',
|
|
||||||
records: 0,
|
|
||||||
errors: 1,
|
|
||||||
warnings: 0,
|
|
||||||
errorMessage: 'Formato de fecha incorrecto en la columna "fecha_venta"'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const supportedFormats = [
|
|
||||||
{
|
|
||||||
type: 'productos',
|
|
||||||
name: 'Productos y Menú',
|
|
||||||
formats: ['CSV', 'Excel'],
|
|
||||||
description: 'Lista de productos con precios, categorías y descripciones',
|
|
||||||
template: 'template_productos.csv',
|
|
||||||
requiredFields: ['nombre', 'categoria', 'precio', 'descripcion']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'inventario',
|
|
||||||
name: 'Inventario Inicial',
|
|
||||||
formats: ['CSV', 'Excel'],
|
|
||||||
description: 'Stock inicial de ingredientes y materias primas',
|
|
||||||
template: 'template_inventario.xlsx',
|
|
||||||
requiredFields: ['item', 'cantidad', 'unidad', 'costo_unitario']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'empleados',
|
|
||||||
name: 'Empleados',
|
|
||||||
formats: ['CSV'],
|
|
||||||
description: 'Información del personal y roles',
|
|
||||||
template: 'template_empleados.csv',
|
|
||||||
requiredFields: ['nombre', 'cargo', 'email', 'telefono']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'ventas',
|
|
||||||
name: 'Historial de Ventas',
|
|
||||||
formats: ['CSV'],
|
|
||||||
description: 'Datos históricos de ventas para análisis',
|
|
||||||
template: 'template_ventas.csv',
|
|
||||||
requiredFields: ['fecha', 'producto', 'cantidad', 'total']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'proveedores',
|
|
||||||
name: 'Proveedores',
|
|
||||||
formats: ['CSV', 'Excel'],
|
|
||||||
description: 'Lista de proveedores y datos de contacto',
|
|
||||||
template: 'template_proveedores.csv',
|
|
||||||
requiredFields: ['nombre', 'contacto', 'telefono', 'email']
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const uploadStats = {
|
|
||||||
totalFiles: uploadedFiles.length,
|
|
||||||
completedFiles: uploadedFiles.filter(f => f.status === 'completed').length,
|
|
||||||
totalRecords: uploadedFiles.reduce((sum, f) => sum + f.records, 0),
|
|
||||||
totalErrors: uploadedFiles.reduce((sum, f) => sum + f.errors, 0),
|
|
||||||
totalWarnings: uploadedFiles.reduce((sum, f) => sum + f.warnings, 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
|
||||||
const iconProps = { className: "w-5 h-5" };
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return <Check {...iconProps} className="w-5 h-5 text-green-600" />;
|
|
||||||
case 'processing': return <RefreshCw {...iconProps} className="w-5 h-5 text-blue-600 animate-spin" />;
|
|
||||||
case 'error': return <AlertCircle {...iconProps} className="w-5 h-5 text-red-600" />;
|
|
||||||
default: return <File {...iconProps} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'green';
|
|
||||||
case 'processing': return 'blue';
|
|
||||||
case 'error': return 'red';
|
|
||||||
default: return 'gray';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusLabel = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'Completado';
|
|
||||||
case 'processing': return 'Procesando';
|
|
||||||
case 'error': return 'Error';
|
|
||||||
default: return status;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDragActive(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragLeave = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDragActive(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDragActive(false);
|
|
||||||
const files = Array.from(e.dataTransfer.files);
|
|
||||||
handleFiles(files);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (e.target.files) {
|
|
||||||
const files = Array.from(e.target.files);
|
|
||||||
handleFiles(files);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFiles = (files: File[]) => {
|
|
||||||
console.log('Files selected:', files);
|
|
||||||
// Simulate upload progress
|
|
||||||
setIsProcessing(true);
|
|
||||||
setUploadProgress(0);
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setUploadProgress(prev => {
|
|
||||||
if (prev >= 100) {
|
|
||||||
clearInterval(interval);
|
|
||||||
setIsProcessing(false);
|
|
||||||
return 100;
|
|
||||||
}
|
|
||||||
return prev + 10;
|
|
||||||
});
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadTemplate = (template: string) => {
|
|
||||||
console.log('Downloading template:', template);
|
|
||||||
// Handle template download
|
|
||||||
};
|
|
||||||
|
|
||||||
const retryUpload = (fileId: string) => {
|
|
||||||
console.log('Retrying upload for file:', fileId);
|
|
||||||
// Handle retry logic
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteFile = (fileId: string) => {
|
|
||||||
console.log('Deleting file:', fileId);
|
|
||||||
// Handle delete logic
|
|
||||||
};
|
|
||||||
|
|
||||||
const viewDetails = (fileId: string) => {
|
|
||||||
console.log('Viewing details for file:', fileId);
|
|
||||||
// Handle view details logic
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
<PageHeader
|
|
||||||
title="Carga de Datos"
|
|
||||||
description="Importa tus datos existentes para acelerar la configuración inicial"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Upload Stats */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Archivos Subidos</p>
|
|
||||||
<p className="text-3xl font-bold text-blue-600">{uploadStats.totalFiles}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
|
|
||||||
<Upload className="h-6 w-6 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Completados</p>
|
|
||||||
<p className="text-3xl font-bold text-green-600">{uploadStats.completedFiles}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
|
|
||||||
<Check className="h-6 w-6 text-green-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Registros</p>
|
|
||||||
<p className="text-3xl font-bold text-purple-600">{uploadStats.totalRecords}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
|
||||||
<File className="h-6 w-6 text-purple-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Errores</p>
|
|
||||||
<p className="text-3xl font-bold text-red-600">{uploadStats.totalErrors}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
|
|
||||||
<AlertCircle className="h-6 w-6 text-red-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Advertencias</p>
|
|
||||||
<p className="text-3xl font-bold text-yellow-600">{uploadStats.totalWarnings}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-12 w-12 bg-yellow-100 rounded-full flex items-center justify-center">
|
|
||||||
<AlertCircle className="h-6 w-6 text-yellow-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Upload Area */}
|
|
||||||
<Card className="p-8">
|
|
||||||
<div
|
|
||||||
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
|
||||||
dragActive
|
|
||||||
? 'border-blue-400 bg-blue-50'
|
|
||||||
: 'border-gray-300 hover:border-gray-400'
|
|
||||||
}`}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
>
|
|
||||||
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
||||||
Arrastra archivos aquí o haz clic para seleccionar
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
Formatos soportados: CSV, Excel (XLSX). Tamaño máximo: 10MB por archivo
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
accept=".csv,.xlsx,.xls"
|
|
||||||
onChange={handleFileInput}
|
|
||||||
className="hidden"
|
|
||||||
id="file-upload"
|
|
||||||
/>
|
|
||||||
<label htmlFor="file-upload">
|
|
||||||
<Button className="cursor-pointer">
|
|
||||||
Seleccionar Archivos
|
|
||||||
</Button>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{isProcessing && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2 mb-2">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
||||||
style={{ width: `${uploadProgress}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-600">Procesando... {uploadProgress}%</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Supported Formats */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Formatos Soportados</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{supportedFormats.map((format, index) => (
|
|
||||||
<div key={index} className="border rounded-lg p-4">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h4 className="font-medium text-gray-900">{format.name}</h4>
|
|
||||||
<div className="flex space-x-1">
|
|
||||||
{format.formats.map((fmt, idx) => (
|
|
||||||
<Badge key={idx} variant="blue" className="text-xs">{fmt}</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-gray-600 mb-3">{format.description}</p>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<p className="text-xs font-medium text-gray-700 mb-1">Campos requeridos:</p>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{format.requiredFields.map((field, idx) => (
|
|
||||||
<span key={idx} className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded">
|
|
||||||
{field}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => downloadTemplate(format.template)}
|
|
||||||
>
|
|
||||||
<Download className="w-3 h-3 mr-2" />
|
|
||||||
Descargar Plantilla
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Uploaded Files */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Archivos Cargados</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{uploadedFiles.map((file) => (
|
|
||||||
<div key={file.id} className="flex items-center justify-between p-4 border rounded-lg">
|
|
||||||
<div className="flex items-center space-x-4 flex-1">
|
|
||||||
{getStatusIcon(file.status)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center space-x-3 mb-1">
|
|
||||||
<h4 className="text-sm font-medium text-gray-900">{file.name}</h4>
|
|
||||||
<Badge variant={getStatusColor(file.status)}>
|
|
||||||
{getStatusLabel(file.status)}
|
|
||||||
</Badge>
|
|
||||||
<span className="text-xs text-gray-500">{file.size}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 text-xs text-gray-600">
|
|
||||||
<span>{file.records} registros</span>
|
|
||||||
{file.errors > 0 && (
|
|
||||||
<span className="text-red-600">{file.errors} errores</span>
|
|
||||||
)}
|
|
||||||
{file.warnings > 0 && (
|
|
||||||
<span className="text-yellow-600">{file.warnings} advertencias</span>
|
|
||||||
)}
|
|
||||||
<span>Subido: {new Date(file.uploadedAt).toLocaleString('es-ES')}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{file.status === 'error' && file.errorMessage && (
|
|
||||||
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700">
|
|
||||||
{file.errorMessage}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button size="sm" variant="outline" onClick={() => viewDetails(file.id)}>
|
|
||||||
<Eye className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{file.status === 'error' && (
|
|
||||||
<Button size="sm" variant="outline" onClick={() => retryUpload(file.id)}>
|
|
||||||
<RefreshCw className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button size="sm" variant="outline" onClick={() => deleteFile(file.id)}>
|
|
||||||
<Trash2 className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Help Section */}
|
|
||||||
<Card className="p-6 bg-blue-50 border-blue-200">
|
|
||||||
<h3 className="text-lg font-semibold text-blue-900 mb-4">Tips para la Carga de Datos</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-blue-800">
|
|
||||||
<ul className="space-y-2">
|
|
||||||
<li>• Usa las plantillas proporcionadas para garantizar el formato correcto</li>
|
|
||||||
<li>• Verifica que todos los campos requeridos estén completos</li>
|
|
||||||
<li>• Los archivos CSV deben usar codificación UTF-8</li>
|
|
||||||
<li>• Las fechas deben estar en formato DD/MM/YYYY</li>
|
|
||||||
</ul>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
<li>• Los precios deben usar punto (.) como separador decimal</li>
|
|
||||||
<li>• Evita caracteres especiales en los nombres de productos</li>
|
|
||||||
<li>• Mantén los nombres de archivos descriptivos</li>
|
|
||||||
<li>• Puedes cargar múltiples archivos del mismo tipo</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OnboardingUploadPage;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as OnboardingUploadPage } from './OnboardingUploadPage';
|
|
||||||
@@ -1,710 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { Button } from '../../components/ui';
|
|
||||||
import { PublicLayout } from '../../components/layout';
|
|
||||||
import {
|
|
||||||
BarChart3,
|
|
||||||
TrendingUp,
|
|
||||||
Shield,
|
|
||||||
Zap,
|
|
||||||
Users,
|
|
||||||
Award,
|
|
||||||
ChevronRight,
|
|
||||||
Check,
|
|
||||||
Star,
|
|
||||||
ArrowRight,
|
|
||||||
Play,
|
|
||||||
Calendar,
|
|
||||||
Clock,
|
|
||||||
DollarSign,
|
|
||||||
Package,
|
|
||||||
PieChart,
|
|
||||||
Settings
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
const LandingPage: React.FC = () => {
|
|
||||||
const scrollToSection = (sectionId: string) => {
|
|
||||||
const element = document.getElementById(sectionId);
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PublicLayout
|
|
||||||
variant="full-width"
|
|
||||||
contentPadding="none"
|
|
||||||
headerProps={{
|
|
||||||
showThemeToggle: true,
|
|
||||||
showAuthButtons: true,
|
|
||||||
variant: "default",
|
|
||||||
navigationItems: [
|
|
||||||
{ id: 'features', label: 'Características', href: '#features' },
|
|
||||||
{ id: 'benefits', label: 'Beneficios', href: '#benefits' },
|
|
||||||
{ id: 'pricing', label: 'Precios', href: '#pricing' },
|
|
||||||
{ id: 'testimonials', label: 'Testimonios', href: '#testimonials' }
|
|
||||||
]
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
|
|
||||||
{/* Hero Section */}
|
|
||||||
<section className="relative py-20 lg:py-32 bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="mb-6">
|
|
||||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
|
|
||||||
<Zap className="w-4 h-4 mr-2" />
|
|
||||||
IA Avanzada para Panaderías
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="text-4xl tracking-tight font-extrabold text-[var(--text-primary)] sm:text-5xl lg:text-7xl">
|
|
||||||
<span className="block">Revoluciona tu</span>
|
|
||||||
<span className="block text-[var(--color-primary)]">Panadería con IA</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)] sm:text-xl">
|
|
||||||
Optimiza automáticamente tu producción, reduce desperdicios hasta un 35%,
|
|
||||||
predice demanda con precisión del 92% y aumenta tus ventas con inteligencia artificial.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
|
|
||||||
<Link to="/register">
|
|
||||||
<Button size="lg" className="px-8 py-4 text-lg font-semibold bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200">
|
|
||||||
Comenzar Gratis 14 Días
|
|
||||||
<ArrowRight className="ml-2 w-5 h-5" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="lg"
|
|
||||||
className="px-8 py-4 text-lg font-semibold border-2 border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-white transition-all duration-200"
|
|
||||||
onClick={() => scrollToSection('demo')}
|
|
||||||
>
|
|
||||||
<Play className="mr-2 w-5 h-5" />
|
|
||||||
Ver Demo en Vivo
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-12 flex items-center justify-center space-x-6 text-sm text-[var(--text-tertiary)]">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
|
||||||
Sin tarjeta de crédito
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
|
||||||
Configuración en 5 minutos
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
|
||||||
Soporte 24/7 en español
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Background decoration */}
|
|
||||||
<div className="absolute top-0 left-0 right-0 h-full overflow-hidden -z-10">
|
|
||||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-[var(--color-primary)]/5 rounded-full blur-3xl"></div>
|
|
||||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-[var(--color-secondary)]/5 rounded-full blur-3xl"></div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Stats Section */}
|
|
||||||
<section className="py-16 bg-[var(--bg-primary)]">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-3xl font-bold text-[var(--color-primary)]">500+</div>
|
|
||||||
<div className="mt-2 text-sm text-[var(--text-secondary)]">Panaderías Activas</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-3xl font-bold text-[var(--color-primary)]">35%</div>
|
|
||||||
<div className="mt-2 text-sm text-[var(--text-secondary)]">Reducción de Desperdicios</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-3xl font-bold text-[var(--color-primary)]">92%</div>
|
|
||||||
<div className="mt-2 text-sm text-[var(--text-secondary)]">Precisión de Predicciones</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-3xl font-bold text-[var(--color-primary)]">4.8★</div>
|
|
||||||
<div className="mt-2 text-sm text-[var(--text-secondary)]">Satisfacción de Clientes</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Main Features Section */}
|
|
||||||
<section id="features" className="py-24 bg-[var(--bg-secondary)]">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-3xl lg:text-5xl font-extrabold text-[var(--text-primary)]">
|
|
||||||
Gestión Completa con
|
|
||||||
<span className="block text-[var(--color-primary)]">Inteligencia Artificial</span>
|
|
||||||
</h2>
|
|
||||||
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)]">
|
|
||||||
Automatiza procesos, optimiza recursos y toma decisiones inteligentes basadas en datos reales de tu panadería.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-20 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
||||||
{/* AI Forecasting */}
|
|
||||||
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
|
|
||||||
<div className="absolute -top-4 left-8">
|
|
||||||
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-xl flex items-center justify-center shadow-lg">
|
|
||||||
<TrendingUp className="w-6 h-6 text-white" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
<h3 className="text-xl font-bold text-[var(--text-primary)]">Predicción Inteligente</h3>
|
|
||||||
<p className="mt-4 text-[var(--text-secondary)]">
|
|
||||||
Algoritmos de IA analizan patrones históricos, clima, eventos locales y tendencias para predecir la demanda exacta de cada producto.
|
|
||||||
</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<div className="flex items-center text-sm text-[var(--color-primary)]">
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Precisión del 92% en predicciones
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-sm text-[var(--color-primary)] mt-2">
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Reduce desperdicios hasta 35%
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-sm text-[var(--color-primary)] mt-2">
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Aumenta ventas promedio 22%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Smart Inventory */}
|
|
||||||
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
|
|
||||||
<div className="absolute -top-4 left-8">
|
|
||||||
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-secondary)] to-[var(--color-secondary-dark)] rounded-xl flex items-center justify-center shadow-lg">
|
|
||||||
<Package className="w-6 h-6 text-white" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
<h3 className="text-xl font-bold text-[var(--text-primary)]">Inventario Inteligente</h3>
|
|
||||||
<p className="mt-4 text-[var(--text-secondary)]">
|
|
||||||
Control automático de stock con alertas predictivas, órdenes de compra automatizadas y optimización de costos.
|
|
||||||
</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<div className="flex items-center text-sm text-[var(--color-secondary)]">
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Alertas automáticas de stock bajo
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-sm text-[var(--color-secondary)] mt-2">
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Órdenes de compra automatizadas
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-sm text-[var(--color-secondary)] mt-2">
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Optimización de costos de materias primas
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Production Planning */}
|
|
||||||
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
|
|
||||||
<div className="absolute -top-4 left-8">
|
|
||||||
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-accent)] to-[var(--color-accent-dark)] rounded-xl flex items-center justify-center shadow-lg">
|
|
||||||
<Calendar className="w-6 h-6 text-white" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
<h3 className="text-xl font-bold text-[var(--text-primary)]">Planificación de Producción</h3>
|
|
||||||
<p className="mt-4 text-[var(--text-secondary)]">
|
|
||||||
Programa automáticamente la producción diaria basada en predicciones, optimiza horarios y recursos disponibles.
|
|
||||||
</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<div className="flex items-center text-sm text-[var(--color-accent)]">
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Programación automática de horneado
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-sm text-[var(--color-accent)] mt-2">
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Optimización de uso de hornos
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-sm text-[var(--color-accent)] mt-2">
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Gestión de personal y turnos
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Additional Features Grid */}
|
|
||||||
<div className="mt-16 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
|
||||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-primary)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
|
||||||
<BarChart3 className="w-6 h-6 text-[var(--color-primary)]" />
|
|
||||||
</div>
|
|
||||||
<h4 className="font-semibold text-[var(--text-primary)]">Analytics Avanzado</h4>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mt-2">Dashboards en tiempo real con métricas clave</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-secondary)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
|
||||||
<DollarSign className="w-6 h-6 text-[var(--color-secondary)]" />
|
|
||||||
</div>
|
|
||||||
<h4 className="font-semibold text-[var(--text-primary)]">POS Integrado</h4>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mt-2">Sistema de ventas completo y fácil de usar</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-accent)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Shield className="w-6 h-6 text-[var(--color-accent)]" />
|
|
||||||
</div>
|
|
||||||
<h4 className="font-semibold text-[var(--text-primary)]">Control de Calidad</h4>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mt-2">Trazabilidad completa y gestión HACCP</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-info)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Settings className="w-6 h-6 text-[var(--color-info)]" />
|
|
||||||
</div>
|
|
||||||
<h4 className="font-semibold text-[var(--text-primary)]">Automatización</h4>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mt-2">Procesos automáticos que ahorran tiempo</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Benefits Section */}
|
|
||||||
<section id="benefits" className="py-24 bg-[var(--bg-primary)]">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="lg:grid lg:grid-cols-2 lg:gap-16 items-center">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
|
||||||
Resultados Comprobados
|
|
||||||
<span className="block text-[var(--color-primary)]">en Cientos de Panaderías</span>
|
|
||||||
</h2>
|
|
||||||
<p className="mt-6 text-lg text-[var(--text-secondary)]">
|
|
||||||
Nuestros clientes han logrado transformaciones significativas en sus operaciones,
|
|
||||||
mejorando rentabilidad y reduciendo desperdicios desde el primer mes.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-10 space-y-8">
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
|
||||||
<TrendingUp className="w-5 h-5 text-green-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="ml-4">
|
|
||||||
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Aumenta Ventas 22% Promedio</h4>
|
|
||||||
<p className="text-[var(--text-secondary)]">Optimización de producción y mejor disponibilidad de productos populares</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
|
||||||
<Shield className="w-5 h-5 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="ml-4">
|
|
||||||
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Reduce Desperdicios 35%</h4>
|
|
||||||
<p className="text-[var(--text-secondary)]">Predicciones precisas evitan sobreproducción y productos vencidos</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
|
||||||
<Clock className="w-5 h-5 text-purple-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="ml-4">
|
|
||||||
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Ahorra 8 Horas Semanales</h4>
|
|
||||||
<p className="text-[var(--text-secondary)]">Automatización de tareas administrativas y de planificación</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-12 lg:mt-0">
|
|
||||||
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-secondary)]/10 rounded-2xl p-8">
|
|
||||||
<div className="grid grid-cols-2 gap-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-3xl font-bold text-[var(--color-primary)]">€127k</div>
|
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Ahorro promedio anual por panadería</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-3xl font-bold text-[var(--color-secondary)]">98%</div>
|
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Satisfacción de clientes</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-3xl font-bold text-[var(--color-accent)]">2.3x</div>
|
|
||||||
<div className="text-sm text-[var(--text-secondary)]">ROI promedio en 12 meses</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-3xl font-bold text-[var(--color-info)]">24/7</div>
|
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Soporte técnico especializado</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Testimonials Section */}
|
|
||||||
<section id="testimonials" className="py-24 bg-[var(--bg-secondary)]">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
|
||||||
Lo que Dicen Nuestros Clientes
|
|
||||||
</h2>
|
|
||||||
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
|
|
||||||
Panaderías de toda España han transformado sus negocios con nuestra plataforma
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-16 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
||||||
{/* Testimonial 1 */}
|
|
||||||
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
|
|
||||||
<div className="flex items-center mb-4">
|
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<blockquote className="text-[var(--text-secondary)] italic">
|
|
||||||
"Desde que implementamos Panadería IA, nuestros desperdicios se redujeron un 40% y las ventas aumentaron un 28%.
|
|
||||||
La predicción de demanda es increíblemente precisa."
|
|
||||||
</blockquote>
|
|
||||||
<div className="mt-6 flex items-center">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-primary)] rounded-full flex items-center justify-center text-white font-bold">
|
|
||||||
M
|
|
||||||
</div>
|
|
||||||
<div className="ml-4">
|
|
||||||
<div className="font-semibold text-[var(--text-primary)]">María González</div>
|
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Panadería Santa María, Madrid</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Testimonial 2 */}
|
|
||||||
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
|
|
||||||
<div className="flex items-center mb-4">
|
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<blockquote className="text-[var(--text-secondary)] italic">
|
|
||||||
"El sistema nos ahorra 10 horas semanales en planificación. Ahora puedo enfocarme en mejorar nuestros productos
|
|
||||||
mientras la IA maneja la logística."
|
|
||||||
</blockquote>
|
|
||||||
<div className="mt-6 flex items-center">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-secondary)] rounded-full flex items-center justify-center text-white font-bold">
|
|
||||||
C
|
|
||||||
</div>
|
|
||||||
<div className="ml-4">
|
|
||||||
<div className="font-semibold text-[var(--text-primary)]">Carlos Ruiz</div>
|
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Horno de Oro, Valencia</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Testimonial 3 */}
|
|
||||||
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
|
|
||||||
<div className="flex items-center mb-4">
|
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<blockquote className="text-[var(--text-secondary)] italic">
|
|
||||||
"Increíble cómo predice exactamente cuántos panes necesitamos cada día. Nuestros clientes siempre encuentran
|
|
||||||
sus productos favoritos disponibles."
|
|
||||||
</blockquote>
|
|
||||||
<div className="mt-6 flex items-center">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-accent)] rounded-full flex items-center justify-center text-white font-bold">
|
|
||||||
A
|
|
||||||
</div>
|
|
||||||
<div className="ml-4">
|
|
||||||
<div className="font-semibold text-[var(--text-primary)]">Ana Martínez</div>
|
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Pan & Tradición, Sevilla</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Trust indicators */}
|
|
||||||
<div className="mt-16 text-center">
|
|
||||||
<p className="text-sm text-[var(--text-tertiary)] mb-8">Confiado por más de 500 panaderías en España</p>
|
|
||||||
<div className="flex items-center justify-center space-x-8 opacity-60">
|
|
||||||
<div className="font-semibold text-[var(--text-secondary)]">Panadería Real</div>
|
|
||||||
<div className="font-semibold text-[var(--text-secondary)]">Horno Artesanal</div>
|
|
||||||
<div className="font-semibold text-[var(--text-secondary)]">Pan de Casa</div>
|
|
||||||
<div className="font-semibold text-[var(--text-secondary)]">Dulce Tradición</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Pricing Section */}
|
|
||||||
<section id="pricing" className="py-24 bg-[var(--bg-primary)]">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
|
||||||
Planes que se Adaptan a tu Negocio
|
|
||||||
</h2>
|
|
||||||
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
|
|
||||||
Sin costos ocultos, sin compromisos largos. Comienza gratis y escala según crezcas.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-16 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
||||||
{/* Starter Plan */}
|
|
||||||
<div className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)]">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Starter</h3>
|
|
||||||
<p className="mt-2 text-sm text-[var(--text-secondary)]">Perfecto para panaderías pequeñas</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<span className="text-3xl font-bold text-[var(--text-primary)]">€49</span>
|
|
||||||
<span className="text-[var(--text-secondary)]">/mes</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 space-y-4">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">Hasta 50 productos</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">Predicción básica de demanda</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">Control de inventario</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">Reportes básicos</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">Soporte por email</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button className="w-full mt-8" variant="outline">
|
|
||||||
Comenzar Gratis
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Professional Plan - Highlighted */}
|
|
||||||
<div className="bg-gradient-to-b from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-2xl p-8 relative shadow-2xl">
|
|
||||||
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
|
||||||
<span className="bg-[var(--color-secondary)] text-white px-4 py-1 rounded-full text-sm font-semibold">
|
|
||||||
Más Popular
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-semibold text-white">Professional</h3>
|
|
||||||
<p className="mt-2 text-sm text-white/80">Para panaderías en crecimiento</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<span className="text-3xl font-bold text-white">€149</span>
|
|
||||||
<span className="text-white/80">/mes</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 space-y-4">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-white mr-3" />
|
|
||||||
<span className="text-sm text-white">Productos ilimitados</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-white mr-3" />
|
|
||||||
<span className="text-sm text-white">IA avanzada con 92% precisión</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-white mr-3" />
|
|
||||||
<span className="text-sm text-white">Gestión completa de producción</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-white mr-3" />
|
|
||||||
<span className="text-sm text-white">POS integrado</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-white mr-3" />
|
|
||||||
<span className="text-sm text-white">Analytics avanzado</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-white mr-3" />
|
|
||||||
<span className="text-sm text-white">Soporte prioritario 24/7</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button className="w-full mt-8 bg-white text-[var(--color-primary)] hover:bg-gray-100">
|
|
||||||
Comenzar Prueba Gratuita
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enterprise Plan */}
|
|
||||||
<div className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)]">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Enterprise</h3>
|
|
||||||
<p className="mt-2 text-sm text-[var(--text-secondary)]">Para cadenas y grandes operaciones</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<span className="text-3xl font-bold text-[var(--text-primary)]">€399</span>
|
|
||||||
<span className="text-[var(--text-secondary)]">/mes</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 space-y-4">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">Multi-locación ilimitada</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">IA personalizada por ubicación</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">API personalizada</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">Integración ERPs</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Check className="w-5 h-5 text-green-500 mr-3" />
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">Manager dedicado</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button className="w-full mt-8" variant="outline">
|
|
||||||
Contactar Ventas
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-16 text-center">
|
|
||||||
<p className="text-sm text-[var(--text-tertiary)]">
|
|
||||||
🔒 Todos los planes incluyen cifrado de datos, backups automáticos y cumplimiento RGPD
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* FAQ Section */}
|
|
||||||
<section className="py-24 bg-[var(--bg-secondary)]">
|
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
|
||||||
Preguntas Frecuentes
|
|
||||||
</h2>
|
|
||||||
<p className="mt-4 text-lg text-[var(--text-secondary)]">
|
|
||||||
Todo lo que necesitas saber sobre Panadería IA
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-16 space-y-8">
|
|
||||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
|
||||||
¿Qué tan precisa es la predicción de demanda?
|
|
||||||
</h3>
|
|
||||||
<p className="mt-4 text-[var(--text-secondary)]">
|
|
||||||
Nuestra IA alcanza una precisión del 92% en predicciones de demanda, analizando más de 50 variables incluyendo
|
|
||||||
histórico de ventas, clima, eventos locales, estacionalidad y tendencias de mercado. La precisión mejora continuamente
|
|
||||||
con más datos de tu panadería.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
|
||||||
¿Cuánto tiempo toma implementar el sistema?
|
|
||||||
</h3>
|
|
||||||
<p className="mt-4 text-[var(--text-secondary)]">
|
|
||||||
La configuración inicial toma solo 5 minutos. Nuestro equipo te ayuda a migrar tus datos históricos en 24-48 horas.
|
|
||||||
La IA comienza a generar predicciones útiles después de una semana de datos, alcanzando máxima precisión en 30 días.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
|
||||||
¿Se integra con mi sistema POS actual?
|
|
||||||
</h3>
|
|
||||||
<p className="mt-4 text-[var(--text-secondary)]">
|
|
||||||
Sí, nos integramos con más de 50 sistemas POS populares en España. También incluimos nuestro propio POS optimizado
|
|
||||||
para panaderías. Si usas un sistema específico, nuestro equipo técnico puede crear una integración personalizada.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
|
||||||
¿Qué soporte técnico ofrecen?
|
|
||||||
</h3>
|
|
||||||
<p className="mt-4 text-[var(--text-secondary)]">
|
|
||||||
Ofrecemos soporte 24/7 en español por chat, email y teléfono. Todos nuestros técnicos son expertos en operaciones
|
|
||||||
de panadería. Además, incluimos onboarding personalizado y training para tu equipo sin costo adicional.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
|
||||||
¿Mis datos están seguros?
|
|
||||||
</h3>
|
|
||||||
<p className="mt-4 text-[var(--text-secondary)]">
|
|
||||||
Absolutamente. Utilizamos cifrado AES-256, servidores en la UE, cumplimos 100% con RGPD y realizamos auditorías
|
|
||||||
de seguridad trimestrales. Tus datos nunca se comparten con terceros y tienes control total sobre tu información.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Final CTA Section */}
|
|
||||||
<section className="py-24 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] relative overflow-hidden">
|
|
||||||
<div className="absolute inset-0">
|
|
||||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
|
||||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8 relative">
|
|
||||||
<h2 className="text-3xl lg:text-5xl font-extrabold text-white">
|
|
||||||
Transforma tu Panadería
|
|
||||||
<span className="block text-white/90">Comenzando Hoy</span>
|
|
||||||
</h2>
|
|
||||||
<p className="mt-6 text-lg text-white/80 max-w-2xl mx-auto">
|
|
||||||
Únete a más de 500 panaderías que ya están reduciendo desperdicios, aumentando ventas y
|
|
||||||
optimizando operaciones con inteligencia artificial.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-12 flex flex-col sm:flex-row gap-6 justify-center">
|
|
||||||
<Link to="/register">
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
className="px-10 py-4 text-lg font-semibold bg-white text-[var(--color-primary)] hover:bg-gray-100 shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200"
|
|
||||||
>
|
|
||||||
Comenzar Prueba Gratuita 14 Días
|
|
||||||
<ArrowRight className="ml-2 w-5 h-5" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
variant="outline"
|
|
||||||
className="px-10 py-4 text-lg font-semibold border-2 border-white text-white hover:bg-white hover:text-[var(--color-primary)] transition-all duration-200"
|
|
||||||
onClick={() => scrollToSection('demo')}
|
|
||||||
>
|
|
||||||
<Play className="mr-2 w-5 h-5" />
|
|
||||||
Ver Demo
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-12 grid grid-cols-1 sm:grid-cols-3 gap-8 text-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-2xl font-bold text-white">14 días</div>
|
|
||||||
<div className="text-white/70 text-sm">Prueba gratuita</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-2xl font-bold text-white">5 min</div>
|
|
||||||
<div className="text-white/70 text-sm">Configuración</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-2xl font-bold text-white">24/7</div>
|
|
||||||
<div className="text-white/70 text-sm">Soporte incluido</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</PublicLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LandingPage;
|
|
||||||
@@ -7,7 +7,7 @@ const RegisterPage: React.FC = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleRegistrationSuccess = () => {
|
const handleRegistrationSuccess = () => {
|
||||||
navigate('/app/onboarding/setup');
|
navigate('/app/onboarding');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoginClick = () => {
|
const handleLoginClick = () => {
|
||||||
|
|||||||
@@ -39,10 +39,7 @@ const TrafficPage = React.lazy(() => import('../pages/app/data/traffic/TrafficPa
|
|||||||
const EventsPage = React.lazy(() => import('../pages/app/data/events/EventsPage'));
|
const EventsPage = React.lazy(() => import('../pages/app/data/events/EventsPage'));
|
||||||
|
|
||||||
// Onboarding pages
|
// Onboarding pages
|
||||||
const OnboardingSetupPage = React.lazy(() => import('../pages/app/onboarding/setup/OnboardingSetupPage'));
|
const OnboardingPage = React.lazy(() => import('../pages/app/onboarding/OnboardingPage'));
|
||||||
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'));
|
|
||||||
|
|
||||||
export const AppRouter: React.FC = () => {
|
export const AppRouter: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
@@ -277,44 +274,12 @@ export const AppRouter: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Onboarding Routes */}
|
{/* Onboarding Route - New Complete Flow */}
|
||||||
<Route
|
<Route
|
||||||
path="/app/onboarding/setup"
|
path="/app/onboarding"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<AppShell>
|
<OnboardingPage />
|
||||||
<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>
|
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -468,57 +468,21 @@ export const routesConfig: RouteConfig[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Onboarding Section - Hidden from navigation
|
// Onboarding Section - Complete 9-step flow
|
||||||
{
|
{
|
||||||
path: '/app/onboarding',
|
path: '/app/onboarding',
|
||||||
name: 'Onboarding',
|
name: 'Onboarding',
|
||||||
component: 'OnboardingPage',
|
component: 'OnboardingPage',
|
||||||
title: 'Configuración Inicial',
|
title: 'Configuración Inicial',
|
||||||
|
description: 'Configuración completa en 9 pasos con IA',
|
||||||
icon: 'settings',
|
icon: 'settings',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
showInNavigation: false,
|
showInNavigation: false,
|
||||||
children: [
|
meta: {
|
||||||
{
|
hideHeader: true,
|
||||||
path: '/app/onboarding/setup',
|
hideSidebar: true,
|
||||||
name: 'OnboardingSetup',
|
fullScreen: true,
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user