From f10a2b92ea7672d94730f1d8c2def02e749da696 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Thu, 18 Dec 2025 13:26:32 +0100 Subject: [PATCH] Improve onboarding --- frontend/src/api/services/onboarding.ts | 2 - frontend/src/api/services/tenant.ts | 26 ++ .../components/domain/auth/RegisterForm.tsx | 5 + .../onboarding/UnifiedOnboardingWizard.tsx | 154 ++++++- .../onboarding/context/WizardContext.tsx | 33 +- .../steps/ChildTenantsSetupStep.tsx | 406 +++++++++++++++++ .../onboarding/steps/CompletionStep.tsx | 16 +- .../steps/ProductionProcessesStep.tsx | 398 ----------------- .../onboarding/steps/RegisterTenantStep.tsx | 51 ++- .../domain/onboarding/steps/index.ts | 4 +- .../setup-wizard/components/StepProgress.tsx | 2 +- .../layout/DemoBanner/DemoBanner.tsx | 94 ++-- .../demo-onboarding/config/driver-config.ts | 2 +- frontend/src/locales/en/demo.json | 26 ++ frontend/src/locales/en/onboarding.json | 4 +- frontend/src/locales/es/demo.json | 26 ++ frontend/src/locales/es/onboarding.json | 4 +- frontend/src/locales/eu/demo.json | 26 ++ frontend/src/locales/eu/onboarding.json | 4 +- .../organizations/OrganizationsPage.tsx | 410 ++++++++---------- .../subscription/SubscriptionPage.tsx | 78 +--- frontend/src/pages/public/DemoPage.tsx | 173 ++------ frontend/src/router/ProtectedRoute.tsx | 36 +- gateway/app/main.py | 13 +- gateway/app/middleware/rate_limiting.py | 260 +++++++++++ gateway/app/routes/tenant.py | 6 + services/auth/README.md | 11 +- services/auth/app/api/onboarding_progress.py | 46 +- services/auth/app/services/auth_service.py | 1 + services/inventory/app/api/ingredients.py | 71 +++ services/recipes/app/api/recipes.py | 43 ++ services/suppliers/app/api/suppliers.py | 51 +++ services/tenant/app/api/internal_demo.py | 13 + services/tenant/app/api/subscription.py | 40 ++ services/tenant/app/api/tenant_hierarchy.py | 246 ++++++++++- services/tenant/app/api/tenant_members.py | 58 +++ services/tenant/app/api/tenant_operations.py | 80 ++++ services/tenant/app/schemas/tenants.py | 98 ++++- .../services/subscription_limit_service.py | 78 +++- .../versions/001_unified_initial_schema.py | 26 +- .../fixtures/enterprise/parent/01-tenant.json | 25 +- .../demo/fixtures/professional/01-tenant.json | 13 +- 42 files changed, 2175 insertions(+), 984 deletions(-) create mode 100644 frontend/src/components/domain/onboarding/steps/ChildTenantsSetupStep.tsx delete mode 100644 frontend/src/components/domain/onboarding/steps/ProductionProcessesStep.tsx create mode 100644 gateway/app/middleware/rate_limiting.py diff --git a/frontend/src/api/services/onboarding.ts b/frontend/src/api/services/onboarding.ts index 80570fbe..2b84b2ad 100644 --- a/frontend/src/api/services/onboarding.ts +++ b/frontend/src/api/services/onboarding.ts @@ -17,7 +17,6 @@ export const BACKEND_ONBOARDING_STEPS = [ 'product-categorization', // Phase 2c: Advanced categorization (optional) 'suppliers-setup', // Phase 2d: Suppliers configuration 'recipes-setup', // Phase 3: Production recipes (optional) - 'production-processes', // Phase 3: Finishing processes (optional) 'quality-setup', // Phase 3: Quality standards (optional) 'team-setup', // Phase 3: Team members (optional) 'ml-training', // Phase 4: AI model training @@ -36,7 +35,6 @@ export const FRONTEND_STEP_ORDER = [ 'product-categorization', // Phase 2c: Advanced categorization (optional) 'suppliers-setup', // Phase 2d: Suppliers configuration 'recipes-setup', // Phase 3: Production recipes (optional) - 'production-processes', // Phase 3: Finishing processes (optional) 'quality-setup', // Phase 3: Quality standards (optional) 'team-setup', // Phase 3: Team members (optional) 'ml-training', // Phase 4: AI model training diff --git a/frontend/src/api/services/tenant.ts b/frontend/src/api/services/tenant.ts index f9b8c858..9d5ada07 100644 --- a/frontend/src/api/services/tenant.ts +++ b/frontend/src/api/services/tenant.ts @@ -86,6 +86,32 @@ export class TenantService { return apiClient.get(`${this.baseUrl}/${parentTenantId}/children`); } + async bulkCreateChildTenants(request: { + parent_tenant_id: string; + child_tenants: Array<{ + name: string; + city: string; + zone?: string; + address: string; + postal_code: string; + location_code: string; + latitude?: number; + longitude?: number; + phone?: string; + email?: string; + }>; + auto_configure_distribution?: boolean; + }): Promise<{ + parent_tenant_id: string; + created_count: number; + failed_count: number; + created_tenants: TenantResponse[]; + failed_tenants: Array<{ name: string; location_code: string; error: string }>; + distribution_configured: boolean; + }> { + return apiClient.post(`${this.baseUrl}/bulk-children`, request); + } + // =================================================================== // OPERATIONS: Search & Discovery // Backend: services/tenant/app/api/tenant_operations.py diff --git a/frontend/src/components/domain/auth/RegisterForm.tsx b/frontend/src/components/domain/auth/RegisterForm.tsx index d525b139..67a77a5b 100644 --- a/frontend/src/components/domain/auth/RegisterForm.tsx +++ b/frontend/src/components/domain/auth/RegisterForm.tsx @@ -209,6 +209,11 @@ export const RegisterForm: React.FC = ({ await register(registrationData); + // CRITICAL: Store subscription_tier in localStorage for onboarding flow + // This is required for conditional step rendering in UnifiedOnboardingWizard + console.log('💾 Storing subscription_tier in localStorage:', selectedPlan); + localStorage.setItem('subscription_tier', selectedPlan); + const successMessage = isPilot ? '¡Bienvenido al programa piloto! Tu cuenta ha sido creada con 3 meses gratis.' : '¡Bienvenido! Tu cuenta ha sido creada correctamente.'; diff --git a/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx b/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx index 2b4b04b8..140df45d 100644 --- a/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx +++ b/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx @@ -11,11 +11,11 @@ import { WizardProvider, useWizardContext, BakeryType, DataSource } from './cont import { BakeryTypeSelectionStep, RegisterTenantStep, + ChildTenantsSetupStep, FileUploadStep, InventoryReviewStep, ProductCategorizationStep, InitialStockEntryStep, - ProductionProcessesStep, MLTrainingStep, CompletionStep } from './steps'; @@ -55,6 +55,26 @@ const OnboardingWizardContent: React.FC = () => { const { user } = useAuth(); const wizardContext = useWizardContext(); + // Get subscription tier from localStorage (set during demo/registration) + const subscriptionTier = localStorage.getItem('subscription_tier') as string; + const isEnterprise = subscriptionTier === 'enterprise'; + + // Auto-set bakeryType for enterprise users (central baker + retail outlets) + // Do this synchronously on mount AND in useEffect to ensure it's set before VISIBLE_STEPS calculation + React.useEffect(() => { + if (isEnterprise && !wizardContext.state.bakeryType) { + console.log('🏢 Auto-setting bakeryType to "mixed" for enterprise tier'); + wizardContext.updateBakeryType('mixed'); // Enterprise is always mixed (production + retail) + } + }, [isEnterprise, wizardContext]); + + // CRITICAL: Set bakeryType synchronously for enterprise on initial render + // This ensures the setup step's condition is satisfied immediately + if (isEnterprise && !wizardContext.state.bakeryType) { + console.log('🏢 [SYNC] Auto-setting bakeryType to "mixed" for enterprise tier'); + wizardContext.updateBakeryType('mixed'); + } + // All possible steps with conditional visibility // All step IDs match backend ONBOARDING_STEPS exactly const ALL_STEPS: StepConfig[] = [ @@ -64,15 +84,50 @@ const OnboardingWizardContent: React.FC = () => { title: t('onboarding:steps.bakery_type.title', 'Tipo de Panadería'), description: t('onboarding:steps.bakery_type.description', 'Selecciona tu tipo de negocio'), component: BakeryTypeSelectionStep, + isConditional: true, + condition: () => { + // Check localStorage directly to ensure we get the current value + const currentTier = localStorage.getItem('subscription_tier'); + return currentTier !== 'enterprise'; // Skip for enterprise users + }, }, // Phase 2: Core Setup { id: 'setup', - title: t('onboarding:steps.setup.title', 'Registrar Panadería'), - description: t('onboarding:steps.setup.description', 'Información básica'), + // Dynamic title based on subscription tier + title: (() => { + const currentTier = localStorage.getItem('subscription_tier'); + return currentTier === 'enterprise' + ? t('onboarding:steps.setup.title_enterprise', 'Registrar Obrador Central') + : t('onboarding:steps.setup.title', 'Registrar Panadería'); + })(), + description: (() => { + const currentTier = localStorage.getItem('subscription_tier'); + return currentTier === 'enterprise' + ? t('onboarding:steps.setup.description_enterprise', 'Información del obrador central') + : t('onboarding:steps.setup.description', 'Información básica'); + })(), component: RegisterTenantStep, isConditional: true, - condition: (ctx) => ctx.state.bakeryType !== null, + condition: (ctx) => { + // Allow setup step if bakeryType is set OR if user is enterprise tier + // (enterprise auto-sets bakeryType to 'mixed' automatically) + const currentTier = localStorage.getItem('subscription_tier'); + return ctx.state.bakeryType !== null || currentTier === 'enterprise'; + }, + }, + // Enterprise-specific: Child Tenants Setup + { + id: 'child-tenants-setup', + title: 'Configurar Sucursales', + description: 'Registra las sucursales de tu red empresarial', + component: ChildTenantsSetupStep, + isConditional: true, + condition: () => { + // Check localStorage directly to ensure we get the current value + const currentTier = localStorage.getItem('subscription_tier'); + return currentTier === 'enterprise'; // Only show for enterprise users + }, }, // POI Detection removed - now happens automatically in background after tenant registration // Phase 2a: AI-Assisted Inventory Setup (REFACTORED - split into 3 focused steps) @@ -116,15 +171,6 @@ const OnboardingWizardContent: React.FC = () => { condition: (ctx) => ctx.state.bakeryType === 'production' || ctx.state.bakeryType === 'mixed', }, - { - id: 'production-processes', - title: t('onboarding:steps.processes.title', 'Procesos'), - description: t('onboarding:steps.processes.description', 'Procesos de terminado'), - component: ProductionProcessesStep, - isConditional: true, - condition: (ctx) => - ctx.state.bakeryType === 'retail' || ctx.state.bakeryType === 'mixed', - }, // Phase 3: Advanced Features (Optional) { id: 'quality-setup', @@ -171,13 +217,15 @@ const OnboardingWizardContent: React.FC = () => { console.log('🔄 VISIBLE_STEPS recalculated:', visibleSteps.map(s => s.id)); console.log('📊 Wizard state:', { + bakeryType: wizardContext.state.bakeryType, + subscriptionTier: localStorage.getItem('subscription_tier'), stockEntryCompleted: wizardContext.state.stockEntryCompleted, aiAnalysisComplete: wizardContext.state.aiAnalysisComplete, categorizationCompleted: wizardContext.state.categorizationCompleted, }); return visibleSteps; - }, [wizardContext.state]); + }, [wizardContext.state, isEnterprise]); // Added isEnterprise to dependencies const isNewTenant = searchParams.get('new') === 'true'; const [currentStepIndex, setCurrentStepIndex] = useState(0); @@ -192,7 +240,7 @@ const OnboardingWizardContent: React.FC = () => { ); const markStepCompleted = useMarkStepCompleted(); - const { setCurrentTenant } = useTenantActions(); + const { setCurrentTenant, loadUserTenants } = useTenantActions(); const [autoCompletionAttempted, setAutoCompletionAttempted] = React.useState(false); // Auto-complete user_registered step @@ -335,12 +383,74 @@ const OnboardingWizardContent: React.FC = () => { if (currentStep.id === 'inventory-setup') { wizardContext.markStepComplete('inventoryCompleted'); } - if (currentStep.id === 'setup' && data?.tenant) { - setCurrentTenant(data.tenant); - - // If tenant info and location are available in data, update the wizard context - if (data.tenantId && data.bakeryLocation) { - wizardContext.updateTenantInfo(data.tenantId, data.bakeryLocation); + if (currentStep.id === 'setup') { + console.log('✅ Setup step completed with data:', { hasTenant: !!data?.tenant, hasUser: !!user?.id, data }); + + if (data?.tenant) { + setCurrentTenant(data.tenant); + + // CRITICAL: Reload user tenants to load membership/access information + // This ensures the user has proper permissions when navigating to dashboard + if (user?.id) { + console.log('🔄 [CRITICAL] Reloading user tenants after tenant creation to load access permissions...'); + await loadUserTenants(); + console.log('✅ [CRITICAL] User tenants reloaded successfully'); + } else { + console.error('❌ [CRITICAL] Cannot reload tenants - user.id is missing!', { user }); + } + + // If tenant info and location are available in data, update the wizard context + if (data.tenantId && data.bakeryLocation) { + wizardContext.updateTenantInfo(data.tenantId, data.bakeryLocation); + } + } else { + console.warn('⚠️ Setup step completed but no tenant data provided!'); + } + } + if (currentStep.id === 'child-tenants-setup' && data?.childTenants) { + wizardContext.updateChildTenants(data.childTenants); + wizardContext.markStepComplete('childTenantsCompleted'); + + // Call backend API to create child tenants + try { + console.log('🏢 Creating child tenants in backend...'); + const { tenantService } = await import('../../../api'); + const parentTenantId = wizardContext.state.tenantId; + + if (!parentTenantId) { + console.error('❌ Parent tenant ID not found in wizard context'); + throw new Error('Parent tenant not registered'); + } + + const response = await tenantService.bulkCreateChildTenants({ + parent_tenant_id: parentTenantId, + child_tenants: data.childTenants.map((ct: any) => ({ + name: ct.name, + city: ct.city, + zone: ct.zone, + address: ct.address, + postal_code: ct.postal_code, + location_code: ct.location_code, + latitude: ct.latitude, + longitude: ct.longitude, + phone: ct.phone, + email: ct.email, + })), + auto_configure_distribution: true, + }); + + console.log('✅ Child tenants created successfully:', { + created: response.created_count, + failed: response.failed_count, + }); + + if (response.failed_count > 0) { + console.warn('⚠️ Some child tenants failed to create:', response.failed_tenants); + } + } catch (childTenantError) { + console.error('❌ Failed to create child tenants:', childTenantError); + // Don't block the onboarding flow - log the error and continue + // The user can add child tenants later from settings } } @@ -485,7 +595,7 @@ const OnboardingWizardContent: React.FC = () => {
- {t('onboarding:wizard.progress.step_of', 'Paso {{current}} de {{total}}', { + {t('onboarding:wizard.progress.step_of', 'Paso {current} de {total}', { current: currentStepIndex + 1, total: VISIBLE_STEPS.length })} diff --git a/frontend/src/components/domain/onboarding/context/WizardContext.tsx b/frontend/src/components/domain/onboarding/context/WizardContext.tsx index c3e1a043..6d91fa1f 100644 --- a/frontend/src/components/domain/onboarding/context/WizardContext.tsx +++ b/frontend/src/components/domain/onboarding/context/WizardContext.tsx @@ -32,6 +32,17 @@ export interface InventoryItemForm { }; } +// Child tenant interface for enterprise onboarding +export interface ChildTenantData { + id: string; + name: string; + city: string; + zone?: string; + address: string; + postal_code: string; + location_code: string; +} + export interface WizardState { // Discovery Phase bakeryType: BakeryType; @@ -44,6 +55,10 @@ export interface WizardState { longitude: number; }; + // Enterprise Setup Data + childTenants?: ChildTenantData[]; // Child tenant locations for enterprise tier + childTenantsCompleted: boolean; + // AI-Assisted Path Data uploadedFile?: File; // NEW: The actual file object needed for sales import API uploadedFileName?: string; @@ -82,6 +97,7 @@ export interface WizardContextValue { updateTenantInfo: (tenantId: string, location: { latitude: number; longitude: number }) => void; updateLocation: (location: { latitude: number; longitude: number }) => void; updateTenantId: (tenantId: string) => void; + updateChildTenants: (tenants: ChildTenantData[]) => void; // NEW: Store child tenants updateAISuggestions: (suggestions: ProductSuggestionResponse[]) => void; // UPDATED type updateUploadedFile: (file: File, validation: ImportValidationResponse) => void; // UPDATED: store file object and validation setAIAnalysisComplete: (complete: boolean) => void; @@ -97,6 +113,8 @@ export interface WizardContextValue { const initialState: WizardState = { bakeryType: null, dataSource: 'ai-assisted', // Only AI-assisted path supported now + childTenants: undefined, + childTenantsCompleted: false, aiSuggestions: [], aiAnalysisComplete: false, categorizedProducts: undefined, @@ -170,12 +188,16 @@ export const WizardProvider: React.FC = ({ }; const updateTenantId = (tenantId: string) => { - setState(prev => ({ - ...prev, + setState(prev => ({ + ...prev, tenantId })); }; + const updateChildTenants = (tenants: ChildTenantData[]) => { + setState(prev => ({ ...prev, childTenants: tenants })); + }; + const updateAISuggestions = (suggestions: ProductSuggestionResponse[]) => { setState(prev => ({ ...prev, aiSuggestions: suggestions })); }; @@ -251,15 +273,11 @@ export const WizardProvider: React.FC = ({ steps.push('suppliers-setup'); steps.push('inventory-setup'); - // Conditional: Recipes vs Processes + // Conditional: Recipes if (state.bakeryType === 'production' || state.bakeryType === 'mixed') { steps.push('recipes-setup'); } - if (state.bakeryType === 'retail' || state.bakeryType === 'mixed') { - steps.push('production-processes'); - } - // Phase 3: Advanced Features (Optional) steps.push('quality-setup'); steps.push('team-setup'); @@ -301,6 +319,7 @@ export const WizardProvider: React.FC = ({ updateTenantInfo, updateLocation, updateTenantId, + updateChildTenants, updateAISuggestions, updateUploadedFile, setAIAnalysisComplete, diff --git a/frontend/src/components/domain/onboarding/steps/ChildTenantsSetupStep.tsx b/frontend/src/components/domain/onboarding/steps/ChildTenantsSetupStep.tsx new file mode 100644 index 00000000..7494eb87 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/ChildTenantsSetupStep.tsx @@ -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 = ({ + onUpdate, + onComplete, + initialData, +}) => { + const { t } = useTranslation(); + const [childTenants, setChildTenants] = useState( + initialData?.childTenants || [] + ); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingTenant, setEditingTenant] = useState(null); + const [formData, setFormData] = useState>({ + name: '', + city: '', + zone: '', + address: '', + postal_code: '', + location_code: '', + }); + const [formErrors, setFormErrors] = useState>({}); + + // 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 = {}; + + 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 ( +
+ {/* Header */} +
+
+ +

+ Configuración de Sucursales +

+
+

+ 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. +

+
+ + {/* Info Box */} +
+
+
+ +
+
+

+ Modelo de Negocio Enterprise +

+

+ Tu obrador central se encargará de la producción, y las sucursales recibirán + los productos terminados mediante transferencias internas optimizadas. +

+
+
+
+ + {/* Child Tenants List */} +
+
+

+ Sucursales ({childTenants.length}) +

+ +
+ + {childTenants.length === 0 ? ( + +
+
+ +
+
+

+ No hay sucursales agregadas +

+

+ Comienza agregando las sucursales que forman parte de tu red empresarial +

+ +
+
+
+ ) : ( +
+ {childTenants.map((tenant) => ( + +
+ {/* Header */} +
+
+
+ +
+
+

+ {tenant.name} +

+ + {tenant.location_code} + +
+
+
+ + +
+
+ + {/* Location Details */} +
+
+ +
+
{tenant.address}
+
+ {tenant.postal_code} - {tenant.city} + {tenant.zone && <>, {tenant.zone}} +
+
+
+
+
+
+ ))} +
+ )} +
+ + {/* Continue Button */} + {childTenants.length > 0 && ( +
+ +
+ )} + + {/* Add/Edit Modal */} + + + +
+ {/* Name */} +
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="ej. Madrid - Salamanca" + error={formErrors.name} + /> +
+ + {/* Location Code */} +
+ + setFormData({ ...formData, location_code: e.target.value.toUpperCase() })} + placeholder="ej. MAD, BCN, VAL" + maxLength={10} + error={formErrors.location_code} + /> +

+ Un código corto para identificar esta ubicación +

+
+ + {/* City and Zone */} +
+
+ + setFormData({ ...formData, city: e.target.value })} + placeholder="ej. Madrid" + error={formErrors.city} + /> +
+
+ + setFormData({ ...formData, zone: e.target.value })} + placeholder="ej. Salamanca" + /> +
+
+ + {/* Address */} +
+ + setFormData({ ...formData, address: e.target.value })} + placeholder="ej. Calle de Serrano, 48" + error={formErrors.address} + /> +
+ + {/* Postal Code */} +
+ + setFormData({ ...formData, postal_code: e.target.value })} + placeholder="ej. 28001" + error={formErrors.postal_code} + /> +
+
+
+ +
+ + +
+
+
+
+ ); +}; + +export default ChildTenantsSetupStep; diff --git a/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx b/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx index 51f3b0b3..9e5d2d8c 100644 --- a/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx @@ -20,12 +20,24 @@ export const CompletionStep: React.FC = ({ 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'); }; diff --git a/frontend/src/components/domain/onboarding/steps/ProductionProcessesStep.tsx b/frontend/src/components/domain/onboarding/steps/ProductionProcessesStep.tsx deleted file mode 100644 index 93664dbe..00000000 --- a/frontend/src/components/domain/onboarding/steps/ProductionProcessesStep.tsx +++ /dev/null @@ -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[] = [ - { - 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 = ({ - onUpdate, - onComplete, - initialData, -}) => { - const { t } = useTranslation(); - const [processes, setProcesses] = useState( - initialData?.processes || [] - ); - const [isAddingNew, setIsAddingNew] = useState(false); - const [showTemplates, setShowTemplates] = useState(true); - const [newProcess, setNewProcess] = useState>({ - 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) => { - 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 ; - case 'decorating': - return ; - case 'finishing': - case 'assembly': - return ; - default: - return ; - } - }; - - return ( -
- {/* Header */} -
-

- {t('onboarding:processes.title', 'Procesos de Producción')} -

-

- {t( - 'onboarding:processes.subtitle', - 'Define los procesos que usas para transformar productos pre-elaborados en productos terminados' - )} -

-
- - {/* Templates Section */} - {showTemplates && processes.length === 0 && ( - -
-
-

- {t('onboarding:processes.templates.title', '⚡ Comienza rápido con plantillas')} -

-

- {t('onboarding:processes.templates.subtitle', 'Haz clic en una plantilla para agregarla')} -

-
- -
-
- {PROCESS_TEMPLATES.map((template, index) => ( - - ))} -
-
- )} - - {/* Existing Processes */} - {processes.length > 0 && ( -
-

- {t('onboarding:processes.your_processes', 'Tus Procesos')} ({processes.length}) -

-
- {processes.map((process) => ( - -
-
-
- {getProcessIcon(process.processType)} -

{process.name}

-
-
- {process.sourceProduct && ( -

- - {t('onboarding:processes.source', 'Desde')}: - {' '} - {process.sourceProduct} -

- )} - {process.finishedProduct && ( -

- - {t('onboarding:processes.finished', 'Hasta')}: - {' '} - {process.finishedProduct} -

- )} -
- ⏱️ {process.duration} min - {process.temperature && 🌡️ {process.temperature}°C} -
- {process.instructions && ( -

{process.instructions}

- )} -
-
- -
-
- ))} -
-
- )} - - {/* Add New Process Form */} - {isAddingNew && ( - -
-

- {t('onboarding:processes.add_new', 'Nuevo Proceso')} -

- -
- -
-
- setNewProcess({ ...newProcess, name: e.target.value })} - placeholder={t('onboarding:processes.form.name_placeholder', 'Ej: Horneado de pan')} - required - /> -
- - setNewProcess({ ...newProcess, sourceProduct: e.target.value })} - placeholder={t('onboarding:processes.form.source_placeholder', 'Ej: Pan pre-cocido')} - required - /> - - setNewProcess({ ...newProcess, finishedProduct: e.target.value })} - placeholder={t('onboarding:processes.form.finished_placeholder', 'Ej: Pan fresco')} - required - /> - - setNewProcess({ ...newProcess, duration: parseInt(e.target.value) })} - min={1} - /> - - {(newProcess.processType === 'baking' || newProcess.processType === 'finishing') && ( - setNewProcess({ ...newProcess, temperature: parseInt(e.target.value) || undefined })} - placeholder="180" - /> - )} - -
- -