Add traslations 2

This commit is contained in:
Urtzi Alfaro
2025-12-25 18:59:56 +01:00
parent b95b86ee2c
commit 8a585058ed
6 changed files with 609 additions and 136 deletions

View File

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

View File

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

View File

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