Improve onboarding

This commit is contained in:
Urtzi Alfaro
2025-12-18 13:26:32 +01:00
parent f76b3f8e6b
commit f10a2b92ea
42 changed files with 2175 additions and 984 deletions

View File

@@ -0,0 +1,406 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus, Store, MapPin, Trash2, Edit2, Building2, X } from 'lucide-react';
import Button from '../../../ui/Button/Button';
import Card from '../../../ui/Card/Card';
import { Modal, ModalHeader, ModalBody, ModalFooter } from '../../../ui/Modal';
import { Input } from '../../../ui/Input';
export interface ChildTenantSetupStepProps {
onUpdate?: (data: { childTenants: ChildTenant[]; canContinue: boolean }) => void;
onComplete?: (data: { childTenants: ChildTenant[] }) => void;
initialData?: {
childTenants?: ChildTenant[];
};
}
export interface ChildTenant {
id: string;
name: string;
city: string;
zone?: string;
address: string;
postal_code: string;
location_code: string;
}
export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
onUpdate,
onComplete,
initialData,
}) => {
const { t } = useTranslation();
const [childTenants, setChildTenants] = useState<ChildTenant[]>(
initialData?.childTenants || []
);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingTenant, setEditingTenant] = useState<ChildTenant | null>(null);
const [formData, setFormData] = useState<Partial<ChildTenant>>({
name: '',
city: '',
zone: '',
address: '',
postal_code: '',
location_code: '',
});
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
// Notify parent when child tenants change
React.useEffect(() => {
onUpdate?.({
childTenants,
canContinue: childTenants.length > 0 // Require at least one child tenant
});
}, [childTenants, onUpdate]);
const validateForm = (): boolean => {
const errors: Record<string, string> = {};
if (!formData.name?.trim()) {
errors.name = 'El nombre es requerido';
}
if (!formData.city?.trim()) {
errors.city = 'La ciudad es requerida';
}
if (!formData.address?.trim()) {
errors.address = 'La dirección es requerida';
}
if (!formData.postal_code?.trim()) {
errors.postal_code = 'El código postal es requerido';
}
if (!formData.location_code?.trim()) {
errors.location_code = 'El código de ubicación es requerido';
} else if (formData.location_code.length > 10) {
errors.location_code = 'El código no debe exceder 10 caracteres';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
const handleOpenModal = (tenant?: ChildTenant) => {
if (tenant) {
setEditingTenant(tenant);
setFormData({ ...tenant });
} else {
setEditingTenant(null);
setFormData({
name: '',
city: '',
zone: '',
address: '',
postal_code: '',
location_code: '',
});
}
setFormErrors({});
setIsModalOpen(true);
};
const handleCloseModal = () => {
setIsModalOpen(false);
setEditingTenant(null);
setFormData({
name: '',
city: '',
zone: '',
address: '',
postal_code: '',
location_code: '',
});
setFormErrors({});
};
const handleSaveTenant = () => {
if (!validateForm()) return;
const tenant: ChildTenant = {
id: editingTenant?.id || `child-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
name: formData.name!,
city: formData.city!,
zone: formData.zone,
address: formData.address!,
postal_code: formData.postal_code!,
location_code: formData.location_code!.toUpperCase(),
};
if (editingTenant) {
// Update existing
setChildTenants(prev => prev.map(t => t.id === editingTenant.id ? tenant : t));
} else {
// Add new
setChildTenants(prev => [...prev, tenant]);
}
handleCloseModal();
};
const handleDeleteTenant = (id: string) => {
setChildTenants(prev => prev.filter(t => t.id !== id));
};
const handleContinue = () => {
if (childTenants.length === 0) {
alert('Debes agregar al menos una sucursal para continuar');
return;
}
onComplete?.({ childTenants });
};
return (
<div className="max-w-4xl mx-auto p-4 md:p-6 space-y-6">
{/* Header */}
<div className="text-center space-y-3">
<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
</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.
</p>
</div>
{/* Info Box */}
<div className="bg-[var(--color-info)]/10 border border-[var(--color-info)]/20 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-[var(--color-info)] mt-0.5">
<Store className="w-5 h-5" />
</div>
<div className="space-y-1">
<h4 className="font-semibold text-[var(--text-primary)]">
Modelo de Negocio Enterprise
</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.
</p>
</div>
</div>
</div>
{/* Child Tenants List */}
<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})
</h2>
<Button
onClick={() => handleOpenModal()}
variant="primary"
size="md"
leftIcon={<Plus className="w-4 h-4" />}
>
Agregar Sucursal
</Button>
</div>
{childTenants.length === 0 ? (
<Card className="p-8 text-center">
<div className="space-y-4">
<div className="w-16 h-16 mx-auto bg-[var(--bg-tertiary)] rounded-full flex items-center justify-center">
<Store className="w-8 h-8 text-[var(--text-tertiary)]" />
</div>
<div>
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
No hay sucursales agregadas
</h3>
<p className="text-sm text-[var(--text-secondary)] mb-4">
Comienza agregando las sucursales que forman parte de tu red empresarial
</p>
<Button
onClick={() => handleOpenModal()}
variant="primary"
size="lg"
leftIcon={<Plus className="w-5 h-5" />}
>
Agregar Primera Sucursal
</Button>
</div>
</div>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{childTenants.map((tenant) => (
<Card key={tenant.id} className="p-4 hover:shadow-md transition-shadow">
<div className="space-y-3">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
<div className="w-10 h-10 bg-[var(--color-primary)]/10 rounded-lg flex items-center justify-center">
<Store className="w-5 h-5 text-[var(--color-primary)]" />
</div>
<div>
<h3 className="font-semibold text-[var(--text-primary)]">
{tenant.name}
</h3>
<span className="text-xs text-[var(--text-tertiary)] font-mono">
{tenant.location_code}
</span>
</div>
</div>
<div className="flex gap-1">
<button
onClick={() => handleOpenModal(tenant)}
className="p-2 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
title="Editar"
>
<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"
>
<Trash2 className="w-4 h-4 text-[var(--color-error)]" />
</button>
</div>
</div>
{/* Location Details */}
<div className="space-y-1 text-sm">
<div className="flex items-start gap-2 text-[var(--text-secondary)]">
<MapPin className="w-4 h-4 mt-0.5 flex-shrink-0" />
<div>
<div>{tenant.address}</div>
<div>
{tenant.postal_code} - {tenant.city}
{tenant.zone && <>, {tenant.zone}</>}
</div>
</div>
</div>
</div>
</div>
</Card>
))}
</div>
)}
</div>
{/* Continue Button */}
{childTenants.length > 0 && (
<div className="flex justify-center pt-4">
<Button
onClick={handleContinue}
variant="primary"
size="lg"
className="w-full sm:w-auto sm:min-w-[200px]"
>
Continuar con {childTenants.length} {childTenants.length === 1 ? 'Sucursal' : 'Sucursales'}
</Button>
</div>
)}
{/* Add/Edit Modal */}
<Modal
isOpen={isModalOpen}
onClose={handleCloseModal}
size="lg"
>
<ModalHeader
title={editingTenant ? 'Editar Sucursal' : 'Agregar Sucursal'}
showCloseButton
onClose={handleCloseModal}
/>
<ModalBody padding="lg">
<div className="space-y-4">
{/* Name */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Nombre de la Sucursal *
</label>
<Input
value={formData.name || ''}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="ej. Madrid - Salamanca"
error={formErrors.name}
/>
</div>
{/* 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)
</label>
<Input
value={formData.location_code || ''}
onChange={(e) => setFormData({ ...formData, location_code: e.target.value.toUpperCase() })}
placeholder="ej. MAD, BCN, VAL"
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
</p>
</div>
{/* City and Zone */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Ciudad *
</label>
<Input
value={formData.city || ''}
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
placeholder="ej. Madrid"
error={formErrors.city}
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Zona / Barrio
</label>
<Input
value={formData.zone || ''}
onChange={(e) => setFormData({ ...formData, zone: e.target.value })}
placeholder="ej. Salamanca"
/>
</div>
</div>
{/* Address */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Dirección *
</label>
<Input
value={formData.address || ''}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
placeholder="ej. Calle de Serrano, 48"
error={formErrors.address}
/>
</div>
{/* Postal Code */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Código Postal *
</label>
<Input
value={formData.postal_code || ''}
onChange={(e) => setFormData({ ...formData, postal_code: e.target.value })}
placeholder="ej. 28001"
error={formErrors.postal_code}
/>
</div>
</div>
</ModalBody>
<ModalFooter justify="end">
<div className="flex gap-3">
<Button variant="outline" onClick={handleCloseModal}>
Cancelar
</Button>
<Button variant="primary" onClick={handleSaveTenant}>
{editingTenant ? 'Guardar Cambios' : 'Agregar Sucursal'}
</Button>
</div>
</ModalFooter>
</Modal>
</div>
);
};
export default ChildTenantsSetupStep;

View File

@@ -20,12 +20,24 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
const navigate = useNavigate();
const currentTenant = useCurrentTenant();
const handleStartUsingSystem = () => {
const handleStartUsingSystem = async () => {
// CRITICAL: Ensure tenant access is loaded before navigating
console.log('🔄 [CompletionStep] Ensuring tenant setup is complete before dashboard navigation...');
// Small delay to ensure any pending state updates complete
await new Promise(resolve => setTimeout(resolve, 500));
onComplete({ redirectTo: '/app/dashboard' });
navigate('/app/dashboard');
};
const handleExploreDashboard = () => {
const handleExploreDashboard = async () => {
// CRITICAL: Ensure tenant access is loaded before navigating
console.log('🔄 [CompletionStep] Ensuring tenant setup is complete before dashboard navigation...');
// Small delay to ensure any pending state updates complete
await new Promise(resolve => setTimeout(resolve, 500));
onComplete({ redirectTo: '/app/dashboard' });
navigate('/app/dashboard');
};

View File

@@ -1,398 +0,0 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus, X, Clock, Flame, ChefHat } from 'lucide-react';
import Button from '../../../ui/Button/Button';
import Card from '../../../ui/Card/Card';
import Input from '../../../ui/Input/Input';
import Select from '../../../ui/Select/Select';
export interface ProductionProcess {
id: string;
name: string;
sourceProduct: string;
finishedProduct: string;
processType: 'baking' | 'decorating' | 'finishing' | 'assembly';
duration: number; // minutes
temperature?: number; // celsius
instructions?: string;
}
export interface ProductionProcessesStepProps {
onUpdate?: (data: { processes: ProductionProcess[] }) => void;
onComplete?: () => void;
initialData?: {
processes?: ProductionProcess[];
};
}
const PROCESS_TEMPLATES: Partial<ProductionProcess>[] = [
{
name: 'Horneado de Pan Pre-cocido',
processType: 'baking',
duration: 15,
temperature: 200,
instructions: 'Hornear a 200°C durante 15 minutos hasta dorar',
},
{
name: 'Terminado de Croissant Congelado',
processType: 'baking',
duration: 20,
temperature: 180,
instructions: 'Descongelar 2h, hornear a 180°C por 20 min',
},
{
name: 'Decoración de Pastel',
processType: 'decorating',
duration: 30,
instructions: 'Aplicar crema, decorar y refrigerar',
},
{
name: 'Montaje de Sándwich',
processType: 'assembly',
duration: 5,
instructions: 'Ensamblar ingredientes según especificación',
},
];
export const ProductionProcessesStep: React.FC<ProductionProcessesStepProps> = ({
onUpdate,
onComplete,
initialData,
}) => {
const { t } = useTranslation();
const [processes, setProcesses] = useState<ProductionProcess[]>(
initialData?.processes || []
);
const [isAddingNew, setIsAddingNew] = useState(false);
const [showTemplates, setShowTemplates] = useState(true);
const [newProcess, setNewProcess] = useState<Partial<ProductionProcess>>({
name: '',
sourceProduct: '',
finishedProduct: '',
processType: 'baking',
duration: 15,
temperature: 180,
instructions: '',
});
const processTypeOptions = [
{ value: 'baking', label: t('onboarding:processes.type.baking', 'Horneado') },
{ value: 'decorating', label: t('onboarding:processes.type.decorating', 'Decoración') },
{ value: 'finishing', label: t('onboarding:processes.type.finishing', 'Terminado') },
{ value: 'assembly', label: t('onboarding:processes.type.assembly', 'Montaje') },
];
const handleAddFromTemplate = (template: Partial<ProductionProcess>) => {
const newProc: ProductionProcess = {
id: `process-${Date.now()}`,
name: template.name || '',
sourceProduct: '',
finishedProduct: '',
processType: template.processType || 'baking',
duration: template.duration || 15,
temperature: template.temperature,
instructions: template.instructions || '',
};
const updated = [...processes, newProc];
setProcesses(updated);
onUpdate?.({ processes: updated });
setShowTemplates(false);
};
const handleAddNew = () => {
if (!newProcess.name || !newProcess.sourceProduct || !newProcess.finishedProduct) {
return;
}
const process: ProductionProcess = {
id: `process-${Date.now()}`,
name: newProcess.name,
sourceProduct: newProcess.sourceProduct,
finishedProduct: newProcess.finishedProduct,
processType: newProcess.processType || 'baking',
duration: newProcess.duration || 15,
temperature: newProcess.temperature,
instructions: newProcess.instructions || '',
};
const updated = [...processes, process];
setProcesses(updated);
onUpdate?.({ processes: updated });
// Reset form
setNewProcess({
name: '',
sourceProduct: '',
finishedProduct: '',
processType: 'baking',
duration: 15,
temperature: 180,
instructions: '',
});
setIsAddingNew(false);
};
const handleRemove = (id: string) => {
const updated = processes.filter(p => p.id !== id);
setProcesses(updated);
onUpdate?.({ processes: updated });
};
const handleContinue = () => {
onComplete?.();
};
const getProcessIcon = (type: string) => {
switch (type) {
case 'baking':
return <Flame className="w-5 h-5 text-orange-500" />;
case 'decorating':
return <ChefHat className="w-5 h-5 text-pink-500" />;
case 'finishing':
case 'assembly':
return <Clock className="w-5 h-5 text-blue-500" />;
default:
return <Clock className="w-5 h-5 text-gray-500" />;
}
};
return (
<div className="max-w-4xl mx-auto p-4 md:p-6 space-y-4 md:space-y-6">
{/* Header */}
<div className="text-center space-y-2">
<h1 className="text-xl md:text-2xl font-bold text-text-primary px-2">
{t('onboarding:processes.title', 'Procesos de Producción')}
</h1>
<p className="text-sm md:text-base text-text-secondary px-4">
{t(
'onboarding:processes.subtitle',
'Define los procesos que usas para transformar productos pre-elaborados en productos terminados'
)}
</p>
</div>
{/* Templates Section */}
{showTemplates && processes.length === 0 && (
<Card className="p-6 space-y-4 bg-gradient-to-br from-blue-50 to-cyan-50">
<div className="flex items-center justify-between">
<div className="space-y-1">
<h3 className="font-semibold text-text-primary">
{t('onboarding:processes.templates.title', '⚡ Comienza rápido con plantillas')}
</h3>
<p className="text-sm text-text-secondary">
{t('onboarding:processes.templates.subtitle', 'Haz clic en una plantilla para agregarla')}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setShowTemplates(false)}
>
{t('onboarding:processes.templates.hide', 'Ocultar')}
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{PROCESS_TEMPLATES.map((template, index) => (
<button
key={index}
onClick={() => handleAddFromTemplate(template)}
className="p-4 text-left bg-white border border-border-primary rounded-lg hover:shadow-md hover:border-primary-300 transition-all"
>
<div className="space-y-2">
<div className="flex items-center gap-2">
{getProcessIcon(template.processType || 'baking')}
<span className="font-medium text-text-primary">{template.name}</span>
</div>
<div className="flex items-center gap-3 text-xs text-text-secondary">
<span> {template.duration} min</span>
{template.temperature && <span>🌡 {template.temperature}°C</span>}
</div>
</div>
</button>
))}
</div>
</Card>
)}
{/* Existing Processes */}
{processes.length > 0 && (
<div className="space-y-3">
<h3 className="font-semibold text-text-primary">
{t('onboarding:processes.your_processes', 'Tus Procesos')} ({processes.length})
</h3>
<div className="space-y-2">
{processes.map((process) => (
<Card key={process.id} className="p-4">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
{getProcessIcon(process.processType)}
<h4 className="font-semibold text-text-primary">{process.name}</h4>
</div>
<div className="text-sm text-text-secondary space-y-1">
{process.sourceProduct && (
<p>
<span className="font-medium">
{t('onboarding:processes.source', 'Desde')}:
</span>{' '}
{process.sourceProduct}
</p>
)}
{process.finishedProduct && (
<p>
<span className="font-medium">
{t('onboarding:processes.finished', 'Hasta')}:
</span>{' '}
{process.finishedProduct}
</p>
)}
<div className="flex items-center gap-3 pt-1">
<span> {process.duration} min</span>
{process.temperature && <span>🌡 {process.temperature}°C</span>}
</div>
{process.instructions && (
<p className="text-xs italic pt-1">{process.instructions}</p>
)}
</div>
</div>
<button
onClick={() => handleRemove(process.id)}
className="text-text-secondary hover:text-red-600 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
</Card>
))}
</div>
</div>
)}
{/* Add New Process Form */}
{isAddingNew && (
<Card className="p-6 space-y-4 border-2 border-primary-300">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-text-primary">
{t('onboarding:processes.add_new', 'Nuevo Proceso')}
</h3>
<button
onClick={() => setIsAddingNew(false)}
className="text-text-secondary hover:text-text-primary"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<Input
label={t('onboarding:processes.form.name', 'Nombre del Proceso')}
value={newProcess.name || ''}
onChange={(e) => setNewProcess({ ...newProcess, name: e.target.value })}
placeholder={t('onboarding:processes.form.name_placeholder', 'Ej: Horneado de pan')}
required
/>
</div>
<Input
label={t('onboarding:processes.form.source', 'Producto Origen')}
value={newProcess.sourceProduct || ''}
onChange={(e) => setNewProcess({ ...newProcess, sourceProduct: e.target.value })}
placeholder={t('onboarding:processes.form.source_placeholder', 'Ej: Pan pre-cocido')}
required
/>
<Input
label={t('onboarding:processes.form.finished', 'Producto Terminado')}
value={newProcess.finishedProduct || ''}
onChange={(e) => setNewProcess({ ...newProcess, finishedProduct: e.target.value })}
placeholder={t('onboarding:processes.form.finished_placeholder', 'Ej: Pan fresco')}
required
/>
<Select
label={t('onboarding:processes.form.type', 'Tipo de Proceso')}
value={newProcess.processType || 'baking'}
onChange={(e) => setNewProcess({ ...newProcess, processType: e.target.value as any })}
options={processTypeOptions}
/>
<Input
type="number"
label={t('onboarding:processes.form.duration', 'Duración (minutos)')}
value={newProcess.duration || 15}
onChange={(e) => setNewProcess({ ...newProcess, duration: parseInt(e.target.value) })}
min={1}
/>
{(newProcess.processType === 'baking' || newProcess.processType === 'finishing') && (
<Input
type="number"
label={t('onboarding:processes.form.temperature', 'Temperatura (°C)')}
value={newProcess.temperature || ''}
onChange={(e) => setNewProcess({ ...newProcess, temperature: parseInt(e.target.value) || undefined })}
placeholder="180"
/>
)}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-text-primary mb-1">
{t('onboarding:processes.form.instructions', 'Instrucciones (opcional)')}
</label>
<textarea
value={newProcess.instructions || ''}
onChange={(e) => setNewProcess({ ...newProcess, instructions: e.target.value })}
placeholder={t('onboarding:processes.form.instructions_placeholder', 'Describe el proceso...')}
rows={3}
className="w-full px-3 py-2 border border-border-primary rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
</div>
<div className="flex gap-2 justify-end">
<Button variant="outline" onClick={() => setIsAddingNew(false)}>
{t('onboarding:processes.form.cancel', 'Cancelar')}
</Button>
<Button
onClick={handleAddNew}
disabled={!newProcess.name || !newProcess.sourceProduct || !newProcess.finishedProduct}
>
{t('onboarding:processes.form.add', 'Agregar Proceso')}
</Button>
</div>
</Card>
)}
{/* Add Button */}
{!isAddingNew && (
<Button
onClick={() => setIsAddingNew(true)}
variant="outline"
className="w-full border-dashed"
>
<Plus className="w-5 h-5 mr-2" />
{t('onboarding:processes.add_button', 'Agregar Proceso')}
</Button>
)}
{/* Footer Actions */}
<div className="flex items-center justify-between pt-6 border-t border-border-primary">
<p className="text-sm text-text-secondary">
{processes.length === 0
? t('onboarding:processes.hint', '💡 Agrega al menos un proceso para continuar')
: t('onboarding:processes.count', `${processes.length} proceso(s) configurado(s)`)}
</p>
<div className="flex gap-3">
<Button variant="outline" onClick={handleContinue}>
{t('onboarding:processes.skip', 'Omitir por ahora')}
</Button>
<Button onClick={handleContinue} disabled={processes.length === 0}>
{t('onboarding:processes.continue', 'Continuar')}
</Button>
</div>
</div>
</div>
);
};
export default ProductionProcessesStep;

View File

@@ -15,11 +15,33 @@ interface RegisterTenantStepProps {
isLastStep: boolean;
}
// Map bakeryType to business_model
const getBakeryBusinessModel = (bakeryType: string | null): string => {
switch (bakeryType) {
case 'production':
return 'central_baker_satellite'; // Production-focused bakery
case 'retail':
return 'retail_bakery'; // Retail/finishing bakery
case 'mixed':
return 'hybrid_bakery'; // Mixed model (enterprise or hybrid)
default:
return 'individual_bakery'; // Default fallback
}
};
export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
onComplete,
isFirstStep
}) => {
const wizardContext = useWizardContext();
// Check if user is enterprise tier for conditional labels
const subscriptionTier = localStorage.getItem('subscription_tier');
const isEnterprise = subscriptionTier === 'enterprise';
// Get business_model from wizard context's bakeryType
const businessModel = getBakeryBusinessModel(wizardContext.state.bakeryType);
const [formData, setFormData] = useState<BakeryRegistration>({
name: '',
address: '',
@@ -27,9 +49,20 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
phone: '',
city: 'Madrid',
business_type: 'bakery',
business_model: 'individual_bakery'
business_model: businessModel
});
// Update business_model when bakeryType changes in context
React.useEffect(() => {
const newBusinessModel = getBakeryBusinessModel(wizardContext.state.bakeryType);
if (newBusinessModel !== formData.business_model) {
setFormData(prev => ({
...prev,
business_model: newBusinessModel
}));
}
}, [wizardContext.state.bakeryType, formData.business_model]);
const [errors, setErrors] = useState<Record<string, string>>({});
const registerBakery = useRegisterBakery();
@@ -110,6 +143,12 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
return;
}
console.log('📝 Registering tenant with data:', {
bakeryType: wizardContext.state.bakeryType,
business_model: formData.business_model,
formData
});
try {
const tenant = await registerBakery.mutateAsync(formData);
@@ -167,8 +206,8 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
<div className="space-y-4 md:space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6">
<Input
label="Nombre de la Panadería"
placeholder="Ingresa el nombre de tu panadería"
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"}
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
error={errors.name}
@@ -191,7 +230,7 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
</label>
<AddressAutocomplete
value={formData.address}
placeholder="Enter bakery address..."
placeholder={isEnterprise ? "Dirección del obrador central..." : "Dirección de tu panadería..."}
onAddressSelect={(address) => {
console.log('Selected:', address.display_name);
handleAddressSelect(address);
@@ -236,10 +275,10 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
<Button
onClick={handleSubmit}
isLoading={registerBakery.isPending}
loadingText="Registrando..."
loadingText={isEnterprise ? "Registrando obrador..." : "Registrando..."}
size="lg"
>
Crear Panadería y Continuar
{isEnterprise ? "Crear Obrador Central y Continuar" : "Crear Panadería y Continuar"}
</Button>
</div>
</div>

View File

@@ -5,6 +5,7 @@ export { default as DataSourceChoiceStep } from './DataSourceChoiceStep';
// Core Onboarding Steps
export { RegisterTenantStep } from './RegisterTenantStep';
export { POIDetectionStep } from './POIDetectionStep';
export { ChildTenantsSetupStep } from './ChildTenantsSetupStep';
// Sales Data & Inventory (REFACTORED - split from UploadSalesDataStep)
export { FileUploadStep } from './FileUploadStep';
@@ -17,9 +18,6 @@ export { UploadSalesDataStep } from './UploadSalesDataStep';
export { default as ProductCategorizationStep } from './ProductCategorizationStep';
export { default as InitialStockEntryStep } from './InitialStockEntryStep';
// Production Steps
export { default as ProductionProcessesStep } from './ProductionProcessesStep';
// ML & Finalization
export { MLTrainingStep } from './MLTrainingStep';
export { CompletionStep } from './CompletionStep';