Add traslations 2
This commit is contained in:
@@ -75,25 +75,25 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!formData.name?.trim()) {
|
||||
errors.name = 'El nombre es requerido';
|
||||
errors.name = t('onboarding:child_tenants.validation.name_required');
|
||||
}
|
||||
if (!formData.city?.trim()) {
|
||||
errors.city = 'La ciudad es requerida';
|
||||
errors.city = t('onboarding:child_tenants.validation.city_required');
|
||||
}
|
||||
if (!formData.address?.trim()) {
|
||||
errors.address = 'La dirección es requerida';
|
||||
errors.address = t('onboarding:child_tenants.validation.address_required');
|
||||
}
|
||||
if (!formData.postal_code?.trim()) {
|
||||
errors.postal_code = 'El código postal es requerido';
|
||||
errors.postal_code = t('onboarding:child_tenants.validation.postal_code_required');
|
||||
} else if (!/^\d{5}$/.test(formData.postal_code)) {
|
||||
errors.postal_code = 'El código postal debe tener exactamente 5 dígitos';
|
||||
errors.postal_code = t('onboarding:child_tenants.validation.postal_code_format');
|
||||
}
|
||||
if (!formData.location_code?.trim()) {
|
||||
errors.location_code = 'El código de ubicación es requerido';
|
||||
errors.location_code = t('onboarding:child_tenants.validation.location_code_required');
|
||||
} else if (formData.location_code.length > 10) {
|
||||
errors.location_code = 'El código no debe exceder 10 caracteres';
|
||||
errors.location_code = t('onboarding:child_tenants.validation.location_code_max');
|
||||
} else if (!/^[A-Z0-9\-_.]+$/.test(formData.location_code)) {
|
||||
errors.location_code = 'Solo se permiten letras mayúsculas, números y guiones/guiones bajos';
|
||||
errors.location_code = t('onboarding:child_tenants.validation.location_code_format');
|
||||
}
|
||||
|
||||
// Phone validation
|
||||
@@ -104,7 +104,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
/^(\+34|0034|34)?9\d{8}$/ // Landline
|
||||
];
|
||||
if (!patterns.some(pattern => pattern.test(phone))) {
|
||||
errors.phone = 'Introduce un número de teléfono español válido';
|
||||
errors.phone = t('onboarding:child_tenants.validation.phone_format');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
if (formData.email && formData.email.trim()) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(formData.email)) {
|
||||
errors.email = 'Introduce un correo electrónico válido';
|
||||
errors.email = t('onboarding:child_tenants.validation.email_format');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
|
||||
const handleContinue = () => {
|
||||
if (childTenants.length === 0) {
|
||||
alert('Debes agregar al menos una sucursal para continuar');
|
||||
alert(t('onboarding:child_tenants.alerts.require_one'));
|
||||
return;
|
||||
}
|
||||
onComplete?.({ childTenants });
|
||||
@@ -204,12 +204,11 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Building2 className="w-10 h-10 text-[var(--color-primary)]" />
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-[var(--text-primary)]">
|
||||
Configuración de Sucursales
|
||||
{t('onboarding:child_tenants.title')}
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-base md:text-lg text-[var(--text-secondary)] max-w-2xl mx-auto">
|
||||
Como empresa con tier Enterprise, tienes un obrador central y múltiples sucursales.
|
||||
Agrega la información de cada sucursal que recibirá productos del obrador central.
|
||||
{t('onboarding:child_tenants.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -221,11 +220,10 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-semibold text-[var(--text-primary)]">
|
||||
Modelo de Negocio Enterprise
|
||||
{t('onboarding:child_tenants.info_box.title')}
|
||||
</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Tu obrador central se encargará de la producción, y las sucursales recibirán
|
||||
los productos terminados mediante transferencias internas optimizadas.
|
||||
{t('onboarding:child_tenants.info_box.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,7 +233,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
Sucursales ({childTenants.length})
|
||||
{t('onboarding:child_tenants.list.title')} ({childTenants.length})
|
||||
</h2>
|
||||
<Button
|
||||
onClick={() => handleOpenModal()}
|
||||
@@ -243,7 +241,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
size="md"
|
||||
leftIcon={<Plus className="w-4 h-4" />}
|
||||
>
|
||||
Agregar Sucursal
|
||||
{t('onboarding:child_tenants.list.add_button')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -255,10 +253,10 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
||||
No hay sucursales agregadas
|
||||
{t('onboarding:child_tenants.list.empty_state.title')}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||
Comienza agregando las sucursales que forman parte de tu red empresarial
|
||||
{t('onboarding:child_tenants.list.empty_state.description')}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => handleOpenModal()}
|
||||
@@ -266,7 +264,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
size="lg"
|
||||
leftIcon={<Plus className="w-5 h-5" />}
|
||||
>
|
||||
Agregar Primera Sucursal
|
||||
{t('onboarding:child_tenants.list.empty_state.button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -302,14 +300,14 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
<button
|
||||
onClick={() => handleOpenModal(tenant)}
|
||||
className="p-2 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
|
||||
title="Editar"
|
||||
title={t('onboarding:child_tenants.card.edit')}
|
||||
>
|
||||
<Edit2 className="w-4 h-4 text-[var(--text-secondary)]" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteTenant(tenant.id)}
|
||||
className="p-2 hover:bg-[var(--color-error)]/10 rounded-lg transition-colors"
|
||||
title="Eliminar"
|
||||
title={t('onboarding:child_tenants.card.delete')}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-[var(--color-error)]" />
|
||||
</button>
|
||||
@@ -345,7 +343,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
size="lg"
|
||||
className="w-full sm:w-auto sm:min-w-[200px]"
|
||||
>
|
||||
Continuar con {childTenants.length} {childTenants.length === 1 ? 'Sucursal' : 'Sucursales'}
|
||||
{t('onboarding:child_tenants.continue_button', { count: childTenants.length })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -357,7 +355,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
size="lg"
|
||||
>
|
||||
<ModalHeader
|
||||
title={editingTenant ? 'Editar Sucursal' : 'Agregar Sucursal'}
|
||||
title={t(editingTenant ? 'onboarding:child_tenants.modal.title_edit' : 'onboarding:child_tenants.modal.title_add')}
|
||||
showCloseButton
|
||||
onClose={handleCloseModal}
|
||||
/>
|
||||
@@ -366,12 +364,12 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Nombre de la Sucursal *
|
||||
{t('onboarding:child_tenants.modal.fields.name')} *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="ej. Madrid - Salamanca"
|
||||
placeholder={t('onboarding:child_tenants.modal.placeholders.name')}
|
||||
error={formErrors.name}
|
||||
/>
|
||||
</div>
|
||||
@@ -379,48 +377,48 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
{/* Location Code */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Código de Ubicación * (máx. 10 caracteres)
|
||||
{t('onboarding:child_tenants.modal.fields.location_code')} * {t('onboarding:child_tenants.modal.fields.location_code_max')}
|
||||
</label>
|
||||
<Input
|
||||
value={formData.location_code || ''}
|
||||
onChange={(e) => setFormData({ ...formData, location_code: e.target.value.toUpperCase() })}
|
||||
placeholder="ej. MAD, BCN, VAL"
|
||||
placeholder={t('onboarding:child_tenants.modal.placeholders.location_code')}
|
||||
maxLength={10}
|
||||
error={formErrors.location_code}
|
||||
/>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
||||
Un código corto para identificar esta ubicación
|
||||
{t('onboarding:child_tenants.modal.fields.location_code_help')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Business Type */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Tipo de Negocio
|
||||
{t('onboarding:child_tenants.modal.fields.business_type')}
|
||||
</label>
|
||||
<Select
|
||||
value={formData.business_type || 'bakery'}
|
||||
onChange={(e) => setFormData({ ...formData, business_type: e.target.value })}
|
||||
>
|
||||
<option value="bakery">Panadería</option>
|
||||
<option value="coffee_shop">Cafetería</option>
|
||||
<option value="pastry_shop">Pastelería</option>
|
||||
<option value="restaurant">Restaurante</option>
|
||||
<option value="bakery">{t('onboarding:child_tenants.modal.business_types.bakery')}</option>
|
||||
<option value="coffee_shop">{t('onboarding:child_tenants.modal.business_types.coffee_shop')}</option>
|
||||
<option value="pastry_shop">{t('onboarding:child_tenants.modal.business_types.pastry_shop')}</option>
|
||||
<option value="restaurant">{t('onboarding:child_tenants.modal.business_types.restaurant')}</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Business Model */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Modelo de Negocio
|
||||
{t('onboarding:child_tenants.modal.fields.business_model')}
|
||||
</label>
|
||||
<Select
|
||||
value={formData.business_model || 'retail_bakery'}
|
||||
onChange={(e) => setFormData({ ...formData, business_model: e.target.value })}
|
||||
>
|
||||
<option value="retail_bakery">Panadería Minorista</option>
|
||||
<option value="central_baker_satellite">Obrador Central + Sucursales</option>
|
||||
<option value="hybrid_bakery">Modelo Híbrido</option>
|
||||
<option value="retail_bakery">{t('onboarding:child_tenants.modal.business_models.retail_bakery')}</option>
|
||||
<option value="central_baker_satellite">{t('onboarding:child_tenants.modal.business_models.central_baker_satellite')}</option>
|
||||
<option value="hybrid_bakery">{t('onboarding:child_tenants.modal.business_models.hybrid_bakery')}</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -428,14 +426,14 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Teléfono
|
||||
{t('onboarding:child_tenants.modal.fields.phone')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" />
|
||||
<Input
|
||||
value={formData.phone || ''}
|
||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||
placeholder="ej. +34 123 456 789"
|
||||
placeholder={t('onboarding:child_tenants.modal.placeholders.phone')}
|
||||
error={formErrors.phone}
|
||||
className="pl-10"
|
||||
/>
|
||||
@@ -443,14 +441,14 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Email
|
||||
{t('onboarding:child_tenants.modal.fields.email')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" />
|
||||
<Input
|
||||
value={formData.email || ''}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
placeholder="ej. contacto@panaderia.com"
|
||||
placeholder={t('onboarding:child_tenants.modal.placeholders.email')}
|
||||
error={formErrors.email}
|
||||
className="pl-10"
|
||||
/>
|
||||
@@ -461,7 +459,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
{/* Timezone */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Zona Horaria
|
||||
{t('onboarding:child_tenants.modal.fields.timezone')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Clock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" />
|
||||
@@ -470,10 +468,10 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
onChange={(e) => setFormData({ ...formData, timezone: e.target.value })}
|
||||
className="pl-10"
|
||||
>
|
||||
<option value="Europe/Madrid">Europe/Madrid (UTC+1/UTC+2)</option>
|
||||
<option value="Europe/Paris">Europe/Paris (UTC+1/UTC+2)</option>
|
||||
<option value="Europe/London">Europe/London (UTC+0/UTC+1)</option>
|
||||
<option value="America/New_York">America/New_York (UTC-5/UTC-4)</option>
|
||||
<option value="Europe/Madrid">{t('onboarding:child_tenants.modal.timezones.europe_madrid')}</option>
|
||||
<option value="Europe/Paris">{t('onboarding:child_tenants.modal.timezones.europe_paris')}</option>
|
||||
<option value="Europe/London">{t('onboarding:child_tenants.modal.timezones.europe_london')}</option>
|
||||
<option value="America/New_York">{t('onboarding:child_tenants.modal.timezones.america_new_york')}</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -481,26 +479,26 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
{/* Zone */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Zona / Barrio (opcional)
|
||||
{t('onboarding:child_tenants.modal.fields.zone')}
|
||||
</label>
|
||||
<Input
|
||||
value={formData.zone || ''}
|
||||
onChange={(e) => setFormData({ ...formData, zone: e.target.value })}
|
||||
placeholder="ej. Salamanca, Chamberí, Centro"
|
||||
placeholder={t('onboarding:child_tenants.modal.placeholders.zone')}
|
||||
/>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
||||
Zona o barrio específico dentro de la ciudad
|
||||
{t('onboarding:child_tenants.modal.fields.zone_help')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Address with Autocomplete */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Dirección *
|
||||
{t('onboarding:child_tenants.modal.fields.address')} *
|
||||
</label>
|
||||
<AddressAutocomplete
|
||||
value={formData.address || ''}
|
||||
placeholder="ej. Calle de Serrano, 48"
|
||||
placeholder={t('onboarding:child_tenants.modal.placeholders.address')}
|
||||
onAddressSelect={(address) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
@@ -532,23 +530,23 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Ciudad *
|
||||
{t('onboarding:child_tenants.modal.fields.city')} *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.city || ''}
|
||||
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
|
||||
placeholder="ej. Madrid"
|
||||
placeholder={t('onboarding:child_tenants.modal.placeholders.city')}
|
||||
error={formErrors.city}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Código Postal *
|
||||
{t('onboarding:child_tenants.modal.fields.postal_code')} *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.postal_code || ''}
|
||||
onChange={(e) => setFormData({ ...formData, postal_code: e.target.value })}
|
||||
placeholder="ej. 28001"
|
||||
placeholder={t('onboarding:child_tenants.modal.placeholders.postal_code')}
|
||||
error={formErrors.postal_code}
|
||||
maxLength={5}
|
||||
/>
|
||||
@@ -559,10 +557,10 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
|
||||
<ModalFooter justify="end">
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={handleCloseModal}>
|
||||
Cancelar
|
||||
{t('onboarding:child_tenants.modal.buttons.cancel')}
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSaveTenant}>
|
||||
{editingTenant ? 'Guardar Cambios' : 'Agregar Sucursal'}
|
||||
{t(editingTenant ? 'onboarding:child_tenants.modal.buttons.save' : 'onboarding:child_tenants.modal.buttons.add')}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
@@ -63,18 +63,18 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
setTrainingProgress({
|
||||
stage: 'training',
|
||||
progress: data.data?.progress || 0,
|
||||
message: data.data?.message || 'Entrenando modelo...',
|
||||
message: data.data?.message || t('onboarding:steps.ml_training.messages.training'),
|
||||
currentStep: data.data?.current_step,
|
||||
estimatedTimeRemaining: data.data?.estimated_time_remaining_seconds || data.data?.estimated_time_remaining,
|
||||
estimatedCompletionTime: data.data?.estimated_completion_time
|
||||
});
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const handleCompleted = useCallback((_data: any) => {
|
||||
setTrainingProgress({
|
||||
stage: 'completed',
|
||||
progress: 100,
|
||||
message: 'Entrenamiento completado exitosamente'
|
||||
message: t('onboarding:steps.ml_training.messages.completed')
|
||||
});
|
||||
setIsTraining(false);
|
||||
|
||||
@@ -82,24 +82,24 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
onComplete({
|
||||
jobId: jobId,
|
||||
success: true,
|
||||
message: 'Modelo entrenado correctamente'
|
||||
message: t('onboarding:steps.ml_training.messages.completed')
|
||||
});
|
||||
}, TRAINING_COMPLETION_DELAY_MS);
|
||||
}, [onComplete, jobId]);
|
||||
}, [onComplete, jobId, t]);
|
||||
|
||||
const handleError = useCallback((data: any) => {
|
||||
setError(data.data?.error || data.error || 'Error durante el entrenamiento');
|
||||
setError(data.data?.error || data.error || t('onboarding:errors.training_failed'));
|
||||
setIsTraining(false);
|
||||
setTrainingProgress(null);
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const handleStarted = useCallback((_data: any) => {
|
||||
setTrainingProgress({
|
||||
stage: 'starting',
|
||||
progress: 5,
|
||||
message: 'Iniciando entrenamiento del modelo...'
|
||||
message: t('onboarding:steps.ml_training.messages.starting')
|
||||
});
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
// WebSocket for real-time training progress - only connect when we have a jobId
|
||||
const { isConnected, connectionError } = useTrainingWebSocket(
|
||||
@@ -140,8 +140,8 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
stage: 'completed',
|
||||
progress: 100,
|
||||
message: isConnected
|
||||
? 'Entrenamiento completado exitosamente'
|
||||
: 'Entrenamiento completado exitosamente (detectado por verificación HTTP)'
|
||||
? t('onboarding:steps.ml_training.messages.completed')
|
||||
: t('onboarding:steps.ml_training.messages.completed_http')
|
||||
});
|
||||
setIsTraining(false);
|
||||
|
||||
@@ -149,13 +149,13 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
onComplete({
|
||||
jobId: jobId,
|
||||
success: true,
|
||||
message: 'Modelo entrenado correctamente',
|
||||
message: t('onboarding:steps.ml_training.messages.completed'),
|
||||
detectedViaPolling: true
|
||||
});
|
||||
}, TRAINING_COMPLETION_DELAY_MS);
|
||||
} else if (jobStatus.status === 'failed') {
|
||||
console.log(`❌ Training failure detected (source: ${isConnected ? 'WebSocket' : 'HTTP polling'})`);
|
||||
setError('Error detectado durante el entrenamiento (verificación de estado)');
|
||||
setError(t('onboarding:errors.training_failed'));
|
||||
setIsTraining(false);
|
||||
setTrainingProgress(null);
|
||||
} else if (jobStatus.status === 'running' && jobStatus.progress !== undefined) {
|
||||
@@ -167,12 +167,12 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
...prev,
|
||||
stage: 'training',
|
||||
progress: jobStatus.progress,
|
||||
message: jobStatus.message || 'Entrenando modelo...',
|
||||
message: jobStatus.message || t('onboarding:steps.ml_training.messages.training'),
|
||||
currentStep: jobStatus.current_step
|
||||
}) as TrainingProgress);
|
||||
}
|
||||
}
|
||||
}, [jobStatus, jobId, trainingProgress?.stage, onComplete, isConnected]);
|
||||
}, [jobStatus, jobId, trainingProgress?.stage, onComplete, isConnected, t]);
|
||||
|
||||
// Auto-trigger training when component mounts (run once)
|
||||
const hasAutoStarted = React.useRef(false);
|
||||
@@ -186,7 +186,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
|
||||
const handleStartTraining = async () => {
|
||||
if (!currentTenant?.id) {
|
||||
setError('No se encontró información del tenant');
|
||||
setError(t('onboarding:errors.network_error'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
setTrainingProgress({
|
||||
stage: 'preparing',
|
||||
progress: 0,
|
||||
message: 'Preparando datos para entrenamiento...'
|
||||
message: t('onboarding:steps.ml_training.messages.preparing')
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -213,10 +213,10 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
setTrainingProgress({
|
||||
stage: 'queued',
|
||||
progress: 10,
|
||||
message: 'Trabajo de entrenamiento en cola...'
|
||||
message: t('onboarding:steps.ml_training.messages.queued')
|
||||
});
|
||||
} catch (err) {
|
||||
setError('Error al iniciar el entrenamiento del modelo');
|
||||
setError(t('onboarding:errors.training_failed'));
|
||||
setIsTraining(false);
|
||||
setTrainingProgress(null);
|
||||
}
|
||||
@@ -271,8 +271,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<p className="text-[var(--text-secondary)] mb-6">
|
||||
Perfecto! Ahora entrenaremos automáticamente tu modelo de inteligencia artificial utilizando los datos de ventas
|
||||
e inventario que has proporcionado. Este proceso puede tomar varios minutos.
|
||||
{t('onboarding:steps.ml_training.intro_text')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -285,9 +284,9 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)]"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Iniciando Entrenamiento Automático</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">{t('onboarding:steps.ml_training.status.preparing')}</h3>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
Preparando el entrenamiento de tu modelo con los datos proporcionados...
|
||||
{t('onboarding:steps.ml_training.messages.preparing')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,9 +318,9 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
{trainingProgress.stage === 'completed'
|
||||
? '¡Entrenamiento Completo!'
|
||||
: 'Entrenando Modelo IA'
|
||||
{trainingProgress.stage === 'completed'
|
||||
? t('onboarding:steps.ml_training.status.completed')
|
||||
: t('onboarding:steps.ml_training.title')
|
||||
}
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] text-sm mb-4">
|
||||
@@ -356,7 +355,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
<span className="font-medium">{trainingProgress.currentStep || t('onboarding:steps.ml_training.progress.data_preparation', 'Procesando...')}</span>
|
||||
{jobId && (
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${isConnected ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'}`}>
|
||||
{isConnected ? '● En vivo' : '● Reconectando...'}
|
||||
{isConnected ? `● ${t('onboarding:steps.ml_training.messages.live')}` : `● ${t('onboarding:steps.ml_training.messages.reconnecting')}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -381,7 +380,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>
|
||||
Finalizará: {formatEstimatedCompletionTime(trainingProgress.estimatedCompletionTime)}
|
||||
{t('onboarding:steps.ml_training.messages.estimated_completion')} {formatEstimatedCompletionTime(trainingProgress.estimatedCompletionTime)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -426,12 +425,12 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
|
||||
{/* Training Info */}
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg p-4">
|
||||
<h4 className="font-medium mb-2">¿Qué sucede durante el entrenamiento?</h4>
|
||||
<h4 className="font-medium mb-2">{t('onboarding:steps.ml_training.training_info.title')}</h4>
|
||||
<ul className="text-sm text-[var(--text-secondary)] space-y-1">
|
||||
<li>• Análisis de patrones de ventas históricos</li>
|
||||
<li>• Creación de modelos predictivos de demanda</li>
|
||||
<li>• Optimización de algoritmos de inventario</li>
|
||||
<li>• Validación y ajuste de precisión</li>
|
||||
<li>• {t('onboarding:steps.ml_training.training_info.step1')}</li>
|
||||
<li>• {t('onboarding:steps.ml_training.training_info.step2')}</li>
|
||||
<li>• {t('onboarding:steps.ml_training.training_info.step3')}</li>
|
||||
<li>• {t('onboarding:steps.ml_training.training_info.step4')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Input } from '../../../ui';
|
||||
import { AddressAutocomplete } from '../../../ui/AddressAutocomplete';
|
||||
import { useRegisterBakery, useTenant, useUpdateTenant } from '../../../../api/hooks/tenant';
|
||||
@@ -33,6 +34,7 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
onComplete,
|
||||
isFirstStep
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const wizardContext = useWizardContext();
|
||||
const tenantId = wizardContext.state.tenantId;
|
||||
|
||||
@@ -126,27 +128,27 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
|
||||
// Required fields according to backend BakeryRegistration schema
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = 'El nombre de la panadería es obligatorio';
|
||||
newErrors.name = t('onboarding:steps.tenant_registration.validation.name_required');
|
||||
} else if (formData.name.length < 2 || formData.name.length > 200) {
|
||||
newErrors.name = 'El nombre debe tener entre 2 y 200 caracteres';
|
||||
newErrors.name = t('onboarding:steps.tenant_registration.validation.name_length');
|
||||
}
|
||||
|
||||
if (!formData.address.trim()) {
|
||||
newErrors.address = 'La dirección es obligatoria';
|
||||
newErrors.address = t('onboarding:steps.tenant_registration.validation.address_required');
|
||||
} else if (formData.address.length < 10 || formData.address.length > 500) {
|
||||
newErrors.address = 'La dirección debe tener entre 10 y 500 caracteres';
|
||||
newErrors.address = t('onboarding:steps.tenant_registration.validation.address_length');
|
||||
}
|
||||
|
||||
if (!formData.postal_code.trim()) {
|
||||
newErrors.postal_code = 'El código postal es obligatorio';
|
||||
newErrors.postal_code = t('onboarding:steps.tenant_registration.validation.postal_code_required');
|
||||
} else if (!/^\d{5}$/.test(formData.postal_code)) {
|
||||
newErrors.postal_code = 'El código postal debe tener exactamente 5 dígitos';
|
||||
newErrors.postal_code = t('onboarding:steps.tenant_registration.validation.postal_code_format');
|
||||
}
|
||||
|
||||
if (!formData.phone.trim()) {
|
||||
newErrors.phone = 'El número de teléfono es obligatorio';
|
||||
newErrors.phone = t('onboarding:steps.tenant_registration.validation.phone_required');
|
||||
} else if (formData.phone.length < 9 || formData.phone.length > 20) {
|
||||
newErrors.phone = 'El teléfono debe tener entre 9 y 20 caracteres';
|
||||
newErrors.phone = t('onboarding:steps.tenant_registration.validation.phone_length');
|
||||
} else {
|
||||
// Basic Spanish phone validation
|
||||
const phone = formData.phone.replace(/[\s\-\(\)]/g, '');
|
||||
@@ -155,7 +157,7 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
/^(\+34|0034|34)?9\d{8}$/ // Landline
|
||||
];
|
||||
if (!patterns.some(pattern => pattern.test(phone))) {
|
||||
newErrors.phone = 'Introduce un número de teléfono español válido';
|
||||
newErrors.phone = t('onboarding:steps.tenant_registration.validation.phone_format');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +242,7 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error registering bakery:', error);
|
||||
setErrors({ submit: 'Error al registrar la panadería. Por favor, inténtalo de nuevo.' });
|
||||
setErrors({ submit: t('onboarding:steps.tenant_registration.errors.register') });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -252,12 +254,16 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
<div className="text-2xl flex-shrink-0">{isEnterprise ? '🏭' : '🏪'}</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-[var(--text-primary)] mb-1">
|
||||
{isEnterprise ? 'Registra tu Obrador Central' : 'Registra tu Panadería'}
|
||||
{t(isEnterprise
|
||||
? 'onboarding:steps.tenant_registration.header.title_enterprise'
|
||||
: 'onboarding:steps.tenant_registration.header.title'
|
||||
)}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{isEnterprise
|
||||
? 'Ingresa los datos de tu obrador principal. Después podrás agregar las sucursales.'
|
||||
: 'Completa la información básica de tu panadería para comenzar.'}
|
||||
{t(isEnterprise
|
||||
? 'onboarding:steps.tenant_registration.header.description_enterprise'
|
||||
: 'onboarding:steps.tenant_registration.header.description'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -266,8 +272,14 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 md:gap-6">
|
||||
<div className="transform transition-all duration-200 hover:scale-[1.01]">
|
||||
<Input
|
||||
label={isEnterprise ? "Nombre del Obrador Central" : "Nombre de la Panadería"}
|
||||
placeholder={isEnterprise ? "Ingresa el nombre de tu obrador central" : "Ingresa el nombre de tu panadería"}
|
||||
label={t(isEnterprise
|
||||
? 'onboarding:steps.tenant_registration.fields.business_name_enterprise'
|
||||
: 'onboarding:steps.tenant_registration.fields.business_name'
|
||||
)}
|
||||
placeholder={t(isEnterprise
|
||||
? 'onboarding:steps.tenant_registration.placeholders.business_name_enterprise'
|
||||
: 'onboarding:steps.tenant_registration.placeholders.business_name'
|
||||
)}
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
error={errors.name}
|
||||
@@ -277,9 +289,9 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
|
||||
<div className="transform transition-all duration-200 hover:scale-[1.01]">
|
||||
<Input
|
||||
label="Teléfono"
|
||||
label={t('onboarding:steps.tenant_registration.fields.phone')}
|
||||
type="tel"
|
||||
placeholder="+34 123 456 789"
|
||||
placeholder={t('onboarding:steps.tenant_registration.placeholders.phone')}
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
error={errors.phone}
|
||||
@@ -290,11 +302,14 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
<div className="md:col-span-2 transform transition-all duration-200 hover:scale-[1.01] relative z-20">
|
||||
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-2 flex items-center gap-2">
|
||||
<span className="text-lg">📍</span>
|
||||
Dirección <span className="text-red-500">*</span>
|
||||
{t('onboarding:steps.tenant_registration.fields.address')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<AddressAutocomplete
|
||||
value={formData.address}
|
||||
placeholder={isEnterprise ? "Dirección del obrador central..." : "Dirección de tu panadería..."}
|
||||
placeholder={t(isEnterprise
|
||||
? 'onboarding:steps.tenant_registration.placeholders.address_enterprise'
|
||||
: 'onboarding:steps.tenant_registration.placeholders.address'
|
||||
)}
|
||||
onAddressSelect={(address) => {
|
||||
console.log('Selected:', address.display_name);
|
||||
handleAddressSelect(address);
|
||||
@@ -313,8 +328,8 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
|
||||
<div className="transform transition-all duration-200 hover:scale-[1.01]">
|
||||
<Input
|
||||
label="Código Postal"
|
||||
placeholder="28001"
|
||||
label={t('onboarding:steps.tenant_registration.fields.postal_code')}
|
||||
placeholder={t('onboarding:steps.tenant_registration.placeholders.postal_code')}
|
||||
value={formData.postal_code}
|
||||
onChange={(e) => handleInputChange('postal_code', e.target.value)}
|
||||
error={errors.postal_code}
|
||||
@@ -325,8 +340,8 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
|
||||
<div className="transform transition-all duration-200 hover:scale-[1.01]">
|
||||
<Input
|
||||
label="Ciudad (Opcional)"
|
||||
placeholder="Madrid"
|
||||
label={t('onboarding:steps.tenant_registration.fields.city')}
|
||||
placeholder={t('onboarding:steps.tenant_registration.placeholders.city')}
|
||||
value={formData.city}
|
||||
onChange={(e) => handleInputChange('city', e.target.value)}
|
||||
error={errors.city}
|
||||
@@ -339,7 +354,9 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-2xl flex-shrink-0">⚠️</span>
|
||||
<div>
|
||||
<h4 className="font-semibold text-[var(--color-error)] mb-1">Error al registrar</h4>
|
||||
<h4 className="font-semibold text-[var(--color-error)] mb-1">
|
||||
{t('onboarding:steps.tenant_registration.errors.register_title')}
|
||||
</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{errors.submit}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -350,14 +367,26 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
isLoading={registerBakery.isPending || updateTenant.isPending}
|
||||
loadingText={tenantId ? "Actualizando obrador..." : (isEnterprise ? "Registrando obrador..." : "Registrando...")}
|
||||
loadingText={t(tenantId
|
||||
? 'onboarding:steps.tenant_registration.loading.updating'
|
||||
: (isEnterprise
|
||||
? 'onboarding:steps.tenant_registration.loading.creating_enterprise'
|
||||
: 'onboarding:steps.tenant_registration.loading.creating'
|
||||
)
|
||||
)}
|
||||
size="lg"
|
||||
className="w-full sm:w-auto sm:min-w-[280px] text-base font-semibold transform transition-all duration-300 hover:scale-105 shadow-lg"
|
||||
>
|
||||
{tenantId
|
||||
? (isEnterprise ? "Actualizar Obrador Central y Continuar →" : "Actualizar Panadería y Continuar →")
|
||||
: (isEnterprise ? "Crear Obrador Central y Continuar →" : "Crear Panadería y Continuar →")
|
||||
}
|
||||
{t(tenantId
|
||||
? (isEnterprise
|
||||
? 'onboarding:steps.tenant_registration.buttons.update_enterprise'
|
||||
: 'onboarding:steps.tenant_registration.buttons.update'
|
||||
)
|
||||
: (isEnterprise
|
||||
? 'onboarding:steps.tenant_registration.buttons.create_enterprise'
|
||||
: 'onboarding:steps.tenant_registration.buttons.create'
|
||||
)
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user