From d2083856fadcc55fdc4301fa790e7d8b7430e07d Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Sat, 6 Sep 2025 19:40:47 +0200 Subject: [PATCH] Start integrating the onboarding flow with backend 10 --- frontend/src/api/hooks/dataImport.ts | 88 ++++- frontend/src/api/index.ts | 1 + frontend/src/api/services/dataImport.ts | 5 +- frontend/src/api/services/onboarding.ts | 38 +- frontend/src/api/types/dataImport.ts | 37 +- .../domain/onboarding/OnboardingWizard.tsx | 23 +- .../onboarding/steps/BakerySetupStep.tsx | 88 ++++- .../onboarding/steps/DataProcessingStep.tsx | 184 ++++++++-- .../onboarding/steps/InventorySetupStep.tsx | 329 +++++++++--------- .../domain/onboarding/steps/ReviewStep.tsx | 3 +- .../business/onboarding/useInventorySetup.ts | 58 ++- .../business/onboarding/useOnboarding.ts | 119 ++++--- .../business/onboarding/useSalesProcessing.ts | 59 +++- .../business/onboarding/useTenantCreation.ts | 14 +- .../pages/app/onboarding/OnboardingPage.tsx | 22 +- frontend/src/stores/auth.store.ts | 15 +- 16 files changed, 768 insertions(+), 315 deletions(-) diff --git a/frontend/src/api/hooks/dataImport.ts b/frontend/src/api/hooks/dataImport.ts index 762485c6..1dcf1ff5 100644 --- a/frontend/src/api/hooks/dataImport.ts +++ b/frontend/src/api/hooks/dataImport.ts @@ -108,6 +108,72 @@ export const useImportCsvFile = ( }; // Combined validation and import hook for easier use +// Validation-only hook for onboarding +export const useValidateFileOnly = () => { + const validateCsv = useValidateCsvFile(); + const validateJson = useValidateJsonData(); + + const validateFile = async ( + tenantId: string, + file: File, + options?: { + onProgress?: (stage: string, progress: number, message: string) => void; + } + ): Promise<{ + validationResult?: ImportValidationResponse; + success: boolean; + error?: string; + }> => { + try { + let validationResult: ImportValidationResponse | undefined; + + options?.onProgress?.('validating', 20, 'Validando estructura del archivo...'); + + const fileExtension = file.name.split('.').pop()?.toLowerCase(); + if (fileExtension === 'csv') { + validationResult = await validateCsv.mutateAsync({ tenantId, file }); + } else if (fileExtension === 'json') { + const jsonData = await file.text().then(text => JSON.parse(text)); + validationResult = await validateJson.mutateAsync({ tenantId, data: jsonData }); + } else { + throw new Error('Formato de archivo no soportado. Use CSV o JSON.'); + } + + options?.onProgress?.('validating', 50, 'Verificando integridad de datos...'); + + if (!validationResult.is_valid) { + const errorMessage = validationResult.errors && validationResult.errors.length > 0 + ? validationResult.errors.join(', ') + : 'Error de validación desconocido'; + throw new Error(`Archivo inválido: ${errorMessage}`); + } + + // Report validation success with details + options?.onProgress?.('completed', 100, + `Archivo validado: ${validationResult.valid_records} registros válidos de ${validationResult.total_records} totales` + ); + + return { + validationResult, + success: true, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Error validando archivo'; + options?.onProgress?.('error', 0, errorMessage); + + return { + success: false, + error: errorMessage, + }; + } + }; + + return { + validateFile, + }; +}; + +// Full validation + import hook (for later use) export const useValidateAndImportFile = () => { const validateCsv = useValidateCsvFile(); const validateJson = useValidateJsonData(); @@ -147,9 +213,17 @@ export const useValidateAndImportFile = () => { options?.onProgress?.('validating', 50, 'Verificando integridad de datos...'); - if (!validationResult.valid) { - throw new Error(`Archivo inválido: ${validationResult.errors?.join(', ')}`); + if (!validationResult.is_valid) { + const errorMessage = validationResult.errors && validationResult.errors.length > 0 + ? validationResult.errors.join(', ') + : 'Error de validación desconocido'; + throw new Error(`Archivo inválido: ${errorMessage}`); } + + // Report validation success with details + options?.onProgress?.('validating', 60, + `Archivo validado: ${validationResult.valid_records} registros válidos de ${validationResult.total_records} totales` + ); } // Step 2: Import @@ -180,12 +254,18 @@ export const useValidateAndImportFile = () => { throw new Error('Formato de archivo no soportado. Use CSV o JSON.'); } - options?.onProgress?.('completed', 100, 'Importación completada'); + // Report completion with details + const completionMessage = importResult.success + ? `Importación completada: ${importResult.records_processed} registros procesados` + : `Importación fallida: ${importResult.errors?.join(', ') || 'Error desconocido'}`; + + options?.onProgress?.('completed', 100, completionMessage); return { validationResult, importResult, - success: true, + success: importResult.success, + error: importResult.success ? undefined : (importResult.errors?.join(', ') || 'Error en la importación'), }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Error procesando archivo'; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 88c47708..8d5d0078 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -384,6 +384,7 @@ export { useValidateCsvFile, useImportJsonData, useImportCsvFile, + useValidateFileOnly, useValidateAndImportFile, dataImportKeys, } from './hooks/dataImport'; diff --git a/frontend/src/api/services/dataImport.ts b/frontend/src/api/services/dataImport.ts index f5cdb305..66ad0122 100644 --- a/frontend/src/api/services/dataImport.ts +++ b/frontend/src/api/services/dataImport.ts @@ -27,9 +27,12 @@ export class DataImportService { tenantId: string, file: File ): Promise { + const formData = new FormData(); + formData.append('file', file); + return apiClient.uploadFile( `${this.baseUrl}/${tenantId}/sales/import/validate-csv`, - file + formData ); } diff --git a/frontend/src/api/services/onboarding.ts b/frontend/src/api/services/onboarding.ts index 790ba92e..ae57bd93 100644 --- a/frontend/src/api/services/onboarding.ts +++ b/frontend/src/api/services/onboarding.ts @@ -5,14 +5,16 @@ import { apiClient } from '../client'; import { UserProgress, UpdateStepRequest } from '../types/onboarding'; export class OnboardingService { - private readonly baseUrl = '/onboarding'; + private readonly baseUrl = '/users/me/onboarding'; async getUserProgress(userId: string): Promise { - return apiClient.get(`${this.baseUrl}/progress/${userId}`); + // Backend uses current user from auth token, so userId parameter is ignored + return apiClient.get(`${this.baseUrl}/progress`); } async updateStep(userId: string, stepData: UpdateStepRequest): Promise { - return apiClient.put(`${this.baseUrl}/progress/${userId}/step`, stepData); + // Backend uses current user from auth token, so userId parameter is ignored + return apiClient.put(`${this.baseUrl}/step`, stepData); } async markStepCompleted( @@ -20,14 +22,19 @@ export class OnboardingService { stepName: string, data?: Record ): Promise { - return apiClient.post(`${this.baseUrl}/progress/${userId}/complete`, { + // Backend uses current user from auth token, so userId parameter is ignored + // Backend expects UpdateStepRequest format for completion + return apiClient.put(`${this.baseUrl}/step`, { step_name: stepName, + completed: true, data: data, }); } async resetProgress(userId: string): Promise { - return apiClient.post(`${this.baseUrl}/progress/${userId}/reset`); + // Note: Backend doesn't have a reset endpoint, this might need to be implemented + // For now, we'll throw an error + throw new Error('Reset progress functionality not implemented in backend'); } async getStepDetails(stepName: string): Promise<{ @@ -36,7 +43,8 @@ export class OnboardingService { dependencies: string[]; estimated_time_minutes: number; }> { - return apiClient.get(`${this.baseUrl}/steps/${stepName}`); + // This endpoint doesn't exist in backend, we'll need to implement it or mock it + throw new Error('getStepDetails functionality not implemented in backend'); } async getAllSteps(): Promise> { - return apiClient.get(`${this.baseUrl}/steps`); + // This endpoint doesn't exist in backend, we'll need to implement it or mock it + throw new Error('getAllSteps functionality not implemented in backend'); + } + + async getNextStep(): Promise<{ step: string; completed?: boolean }> { + // This endpoint exists in backend + return apiClient.get(`${this.baseUrl}/next-step`); + } + + async canAccessStep(stepName: string): Promise<{ can_access: boolean; reason?: string }> { + // This endpoint exists in backend + return apiClient.get(`${this.baseUrl}/can-access/${stepName}`); + } + + async completeOnboarding(): Promise<{ success: boolean; message: string }> { + // This endpoint exists in backend + return apiClient.post(`${this.baseUrl}/complete`); } } diff --git a/frontend/src/api/types/dataImport.ts b/frontend/src/api/types/dataImport.ts index 937f7108..836bb739 100644 --- a/frontend/src/api/types/dataImport.ts +++ b/frontend/src/api/types/dataImport.ts @@ -9,11 +9,30 @@ export interface ImportValidationRequest { } export interface ImportValidationResponse { - valid: boolean; + is_valid: boolean; + total_records: number; + valid_records: number; + invalid_records: number; errors: string[]; warnings: string[]; - record_count?: number; - sample_records?: any[]; + summary: { + status: string; + file_format: string; + file_size_bytes: number; + file_size_mb: number; + estimated_processing_time_seconds: number; + validation_timestamp: string; + detected_columns: string[]; + suggestions: string[]; + }; + unique_products: number; + product_list: string[]; + message: string; + details: { + total_records: number; + format: string; + }; + sample_records?: any[]; // Keep for backward compatibility } export interface ImportProcessRequest { @@ -33,6 +52,18 @@ export interface ImportProcessResponse { records_failed: number; import_id?: string; errors?: string[]; + import_summary?: { + total_records: number; + successful_imports: number; + failed_imports: number; + processing_time_seconds: number; + timestamp: string; + }; + details?: { + tenant_id: string; + file_name?: string; + processing_status: string; + }; } export interface ImportStatusResponse { diff --git a/frontend/src/components/domain/onboarding/OnboardingWizard.tsx b/frontend/src/components/domain/onboarding/OnboardingWizard.tsx index 922e9669..b54872d5 100644 --- a/frontend/src/components/domain/onboarding/OnboardingWizard.tsx +++ b/frontend/src/components/domain/onboarding/OnboardingWizard.tsx @@ -26,10 +26,10 @@ interface OnboardingWizardProps { currentStep: number; data: any; onStepChange: (stepIndex: number, stepData: any) => void; - onNext: () => void; - onPrevious: () => void; - onComplete: (data: any) => void; - onGoToStep: (stepIndex: number) => void; + onNext: () => Promise | boolean; + onPrevious: () => boolean; + onComplete: (data: any) => Promise | void; + onGoToStep: (stepIndex: number) => boolean; onExit?: () => void; className?: string; } @@ -82,9 +82,12 @@ export const OnboardingWizard: React.FC = ({ return true; }, [currentStep, stepData]); - const goToNextStep = useCallback(() => { + const goToNextStep = useCallback(async () => { if (validateCurrentStep()) { - onNext(); + const result = onNext(); + if (result instanceof Promise) { + await result; + } } }, [validateCurrentStep, onNext]); @@ -356,7 +359,13 @@ export const OnboardingWizard: React.FC = ({ + + )} + {/* Show error state if tenant creation fails */} {data.bakery?.creationError && (
@@ -248,6 +323,7 @@ export const BakerySetupStep: React.FC = ({

)} + ); }; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/steps/DataProcessingStep.tsx b/frontend/src/components/domain/onboarding/steps/DataProcessingStep.tsx index c53b455e..cf02d8b9 100644 --- a/frontend/src/components/domain/onboarding/steps/DataProcessingStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/DataProcessingStep.tsx @@ -57,6 +57,7 @@ export const DataProcessingStep: React.FC = ({ validationResults, suggestions }, + tenantCreation, isLoading, error, clearError @@ -70,22 +71,44 @@ export const DataProcessingStep: React.FC = ({ // Get tenant ID from multiple sources with fallback const getTenantId = (): string | null => { - const tenantId = currentTenant?.id || user?.tenant_id || null; + // Also check the onboarding data for tenant creation success + const onboardingTenantId = data.bakery?.tenant_id; + const tenantId = currentTenant?.id || user?.tenant_id || onboardingTenantId || null; console.log('DataProcessingStep - getTenantId:', { currentTenant: currentTenant?.id, userTenantId: user?.tenant_id, + onboardingTenantId: onboardingTenantId, finalTenantId: tenantId, isLoadingUserData, authLoading, tenantLoading, - user: user ? { id: user.id, email: user.email } : null + user: user ? { id: user.id, email: user.email } : null, + tenantCreationSuccess: data.tenantCreation?.isSuccess }); return tenantId; }; - // Check if tenant data is available (not loading and has ID) + // Check if tenant data is available (not loading and has ID, OR tenant was created successfully) const isTenantAvailable = (): boolean => { - return !isLoadingUserData && getTenantId() !== null; + const hasAuth = !authLoading && user; + const hasTenantId = getTenantId() !== null; + const tenantCreatedSuccessfully = tenantCreation.isSuccess; + const tenantCreatedInOnboarding = data.bakery?.tenantCreated === true; + + const isAvailable = hasAuth && (hasTenantId || tenantCreatedSuccessfully || tenantCreatedInOnboarding); + console.log('DataProcessingStep - isTenantAvailable:', { + hasAuth, + hasTenantId, + tenantCreatedSuccessfully, + tenantCreatedInOnboarding, + isAvailable, + authLoading, + tenantLoading, + tenantCreationFromHook: tenantCreation, + bakeryData: data.bakery + }); + + return isAvailable; }; // Use onboarding hook state when available, fallback to local state const [localStage, setLocalStage] = useState(data.processingStage || 'upload'); @@ -93,38 +116,67 @@ export const DataProcessingStep: React.FC = ({ const [localResults, setLocalResults] = useState(data.processingResults || null); // Derive current state from onboarding hooks or local state - const stage = onboardingStage || localStage; + // Priority: if local is 'completed' or 'error', use local; otherwise use onboarding state + const stage = (localStage === 'completed' || localStage === 'error') + ? localStage + : (onboardingStage || localStage); const progress = onboardingProgress || 0; const currentMessage = onboardingMessage || ''; const results = (validationResults && suggestions) ? { ...validationResults, aiSuggestions: suggestions, - // Add calculated fields - productsIdentified: validationResults.product_list?.length || 0, + // Add calculated fields from backend response + productsIdentified: validationResults.unique_products || validationResults.product_list?.length || 0, categoriesDetected: suggestions ? new Set(suggestions.map((s: any) => s.category)).size : 0, businessModel: 'production', confidenceScore: 85, - recommendations: [] + recommendations: validationResults.summary?.suggestions || [], + // Backend response details + totalRecords: validationResults.total_records || 0, + validRecords: validationResults.valid_records || 0, + invalidRecords: validationResults.invalid_records || 0, + fileFormat: validationResults.summary?.file_format || 'csv', + fileSizeMb: validationResults.summary?.file_size_mb || 0, + estimatedProcessingTime: validationResults.summary?.estimated_processing_time_seconds || 0, + detectedColumns: validationResults.summary?.detected_columns || [], + validationMessage: validationResults.message || 'Validación completada' } : localResults; + + // Debug logging for state changes + console.log('DataProcessingStep - State debug:', { + localStage, + onboardingStage, + finalStage: stage, + hasValidationResults: !!validationResults, + hasSuggestions: !!suggestions, + hasResults: !!results, + localResults: !!localResults + }); const [dragActive, setDragActive] = useState(false); const fileInputRef = useRef(null); + const lastStateRef = useRef({ stage, progress, currentMessage, results, suggestions, uploadedFile }); useEffect(() => { - // Update parent data when state changes - onDataChange({ - ...data, - processingStage: stage, - processingProgress: progress, - currentMessage: currentMessage, - processingResults: results, - suggestions: suggestions, - files: { - ...data.files, - salesData: uploadedFile - } - }); - }, [stage, progress, currentMessage, results, suggestions, uploadedFile, onDataChange, data]); + // Only update if state actually changed + const currentState = { stage, progress, currentMessage, results, suggestions, uploadedFile }; + if (JSON.stringify(currentState) !== JSON.stringify(lastStateRef.current)) { + lastStateRef.current = currentState; + + // Update parent data when state changes + onDataChange({ + processingStage: stage, + processingProgress: progress, + currentMessage: currentMessage, + processingResults: results, + suggestions: suggestions, + files: { + ...data.files, + salesData: uploadedFile + } + }); + } + }, [stage, progress, currentMessage, results, suggestions, uploadedFile, onDataChange, data.files]); const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); @@ -190,7 +242,12 @@ export const DataProcessingStep: React.FC = ({ return; } - console.log('DataProcessingStep - Starting file processing'); + console.log('DataProcessingStep - Starting file processing', { + fileName: file.name, + fileSize: file.size, + fileType: file.type, + lastModified: file.lastModified + }); // Use the onboarding hook for file processing const success = await processSalesFile(file, (progress, stage, message) => { @@ -199,6 +256,21 @@ export const DataProcessingStep: React.FC = ({ if (success) { setLocalStage('completed'); + + // If we have results from the onboarding hook, store them locally too + if (validationResults && suggestions) { + const processedResults = { + ...validationResults, + aiSuggestions: suggestions, + productsIdentified: validationResults.product_list?.length || 0, + categoriesDetected: suggestions ? new Set(suggestions.map((s: any) => s.category)).size : 0, + businessModel: 'production', + confidenceScore: 85, + recommendations: [] + }; + setLocalResults(processedResults); + } + toast.addToast('El archivo se procesó correctamente', { title: 'Procesamiento completado', type: 'success' @@ -209,6 +281,15 @@ export const DataProcessingStep: React.FC = ({ } catch (error) { console.error('DataProcessingStep - Processing error:', error); + console.error('DataProcessingStep - Error details:', { + error, + errorMessage: error instanceof Error ? error.message : 'Unknown error', + errorStack: error instanceof Error ? error.stack : undefined, + uploadedFile: file?.name, + fileSize: file?.size, + fileType: file?.type, + localUploadedFile: uploadedFile?.name + }); setLocalStage('error'); const errorMessage = error instanceof Error ? error.message : 'Error en el procesamiento de datos'; @@ -471,15 +552,29 @@ export const DataProcessingStep: React.FC = ({ ¡Procesamiento Completado!

- Tus datos han sido procesados exitosamente + {results.validationMessage || 'Tus datos han sido procesados exitosamente'}

+ {results.fileSizeMb && ( +
+ Archivo {results.fileFormat?.toUpperCase()} • {results.fileSizeMb.toFixed(2)} MB + {results.estimatedProcessingTime && ` • ${results.estimatedProcessingTime}s procesamiento`} +
+ )} - {/* Simple Stats Cards */} + {/* Enhanced Stats Cards */}
-

{results.total_records}

-

Registros

+

{results.totalRecords || results.total_records}

+

Total Registros

+
+
+

{results.validRecords || results.valid_records}

+

Válidos

+
+
+

{results.invalidRecords || results.invalid_records || 0}

+

Inválidos

@@ -500,6 +595,41 @@ export const DataProcessingStep: React.FC = ({

Modelo

+ + {/* Additional Details from Backend */} + {(results.detectedColumns?.length > 0 || results.recommendations?.length > 0) && ( +
+ {/* Detected Columns */} + {results.detectedColumns?.length > 0 && ( + +

Columnas Detectadas

+
+ {results.detectedColumns.map((column, index) => ( + + {column} + + ))} +
+
+ )} + + {/* Backend Recommendations */} + {results.recommendations?.length > 0 && ( + +

Recomendaciones

+
    + {results.recommendations.slice(0, 3).map((rec, index) => ( +
  • + + {rec} +
  • + ))} +
+
+ )} +
+ )} )} diff --git a/frontend/src/components/domain/onboarding/steps/InventorySetupStep.tsx b/frontend/src/components/domain/onboarding/steps/InventorySetupStep.tsx index 9690a7c8..05f4679f 100644 --- a/frontend/src/components/domain/onboarding/steps/InventorySetupStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/InventorySetupStep.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Package, Calendar, AlertTriangle, Plus, Edit, Trash2, CheckCircle } from 'lucide-react'; import { Button, Card, Input, Badge } from '../../../ui'; import { OnboardingStepProps } from '../OnboardingWizard'; @@ -31,8 +31,8 @@ interface InventoryItem { const convertProductsToInventory = (approvedProducts: any[]): InventoryItem[] => { return approvedProducts.map((product, index) => ({ id: `inventory-${index}`, - name: product.suggested_name || product.name, - category: product.product_type || 'finished_product', + name: product.suggested_name || product.original_name, + category: product.product_type === 'ingredient' ? 'ingredient' : 'finished_product', current_stock: 0, // To be configured by user min_stock: 1, // Default minimum max_stock: 100, // Default maximum @@ -62,8 +62,9 @@ export const InventorySetupStep: React.FC = ({ const currentTenant = useCurrentTenant(); const { showToast } = useToast(); - // Use the onboarding hooks + // Use the business onboarding hooks const { + updateStepData, createInventoryFromSuggestions, importSalesData, inventorySetup: { @@ -72,6 +73,7 @@ export const InventorySetupStep: React.FC = ({ salesImportResult, isInventoryConfigured }, + allStepData, isLoading, error, clearError @@ -79,11 +81,16 @@ export const InventorySetupStep: React.FC = ({ const createAlert = (alert: any) => { console.log('Alert:', alert); - showToast({ - title: alert.title, - message: alert.message, - type: alert.type - }); + if (showToast && typeof showToast === 'function') { + showToast({ + title: alert.title, + message: alert.message, + type: alert.type + }); + } else { + // Fallback to console if showToast is not available + console.warn(`Toast would show: ${alert.title} - ${alert.message}`); + } }; // Use modal for confirmations and editing @@ -93,6 +100,7 @@ export const InventorySetupStep: React.FC = ({ const [isCreating, setIsCreating] = useState(false); const [editingItem, setEditingItem] = useState(null); const [isAddingNew, setIsAddingNew] = useState(false); + const [inventoryCreationAttempted, setInventoryCreationAttempted] = useState(false); // Generate inventory items from approved products const generateInventoryFromProducts = (approvedProducts: any[]): InventoryItem[] => { @@ -115,20 +123,24 @@ export const InventorySetupStep: React.FC = ({ if (data.inventoryItems) { return data.inventoryItems; } - // Try to get approved products from current step data first, then from review step data - const approvedProducts = data.approvedProducts || data.allStepData?.['review']?.approvedProducts; + // Try to get approved products from business hooks data first, then from component props + const approvedProducts = data.approvedProducts || + allStepData?.['review']?.approvedProducts || + data.allStepData?.['review']?.approvedProducts; return generateInventoryFromProducts(approvedProducts || []); }); // Update items when approved products become available (for when component is already mounted) useEffect(() => { - const approvedProducts = data.approvedProducts || data.allStepData?.['review']?.approvedProducts; + const approvedProducts = data.approvedProducts || + allStepData?.['review']?.approvedProducts || + data.allStepData?.['review']?.approvedProducts; if (approvedProducts && approvedProducts.length > 0 && items.length === 0) { const newItems = generateInventoryFromProducts(approvedProducts); setItems(newItems); } - }, [data.approvedProducts, data.allStepData]); + }, [data.approvedProducts, allStepData, data.allStepData]); const [filterCategory, setFilterCategory] = useState<'all' | 'ingredient' | 'finished_product'>('all'); @@ -136,20 +148,25 @@ export const InventorySetupStep: React.FC = ({ ? items : items.filter(item => item.category === filterCategory); - // Create inventory items via API using hooks + // Create inventory items via API using business hooks const handleCreateInventory = async () => { console.log('InventorySetup - Starting handleCreateInventory'); - console.log('InventorySetup - data:', data); - console.log('InventorySetup - data.allStepData keys:', Object.keys(data.allStepData || {})); - const approvedProducts = data.approvedProducts || data.allStepData?.['review']?.approvedProducts; - console.log('InventorySetup - approvedProducts:', approvedProducts); + const approvedProducts = data.approvedProducts || + allStepData?.['review']?.approvedProducts || + data.allStepData?.['review']?.approvedProducts; + console.log('InventorySetup - approvedProducts:', { + fromDataProp: data.approvedProducts, + fromAllStepData: allStepData?.['review']?.approvedProducts, + fromDataAllStepData: data.allStepData?.['review']?.approvedProducts, + finalProducts: approvedProducts, + allStepDataKeys: Object.keys(allStepData || {}), + dataKeys: Object.keys(data || {}) + }); // Get tenant ID from current tenant context or user const tenantId = currentTenant?.id || user?.tenant_id; - console.log('InventorySetup - tenantId from currentTenant:', currentTenant?.id); - console.log('InventorySetup - tenantId from user:', user?.tenant_id); - console.log('InventorySetup - final tenantId:', tenantId); + console.log('InventorySetup - tenantId:', tenantId); if (!tenantId || !approvedProducts || approvedProducts.length === 0) { console.log('InventorySetup - Missing requirements: tenantId =', tenantId, 'approvedProducts length =', approvedProducts?.length); @@ -167,162 +184,112 @@ export const InventorySetupStep: React.FC = ({ setIsCreating(true); try { - // Create ingredients one by one using the inventory hook - let successCount = 0; - let failCount = 0; - const createdItems: any[] = []; - const inventoryMapping: { [productName: string]: string } = {}; - - for (const [index, product] of approvedProducts.entries()) { - const ingredientData = { - name: product.suggested_name || product.name, - category: product.category || 'general', - unit_of_measure: product.unit_of_measure || 'unit', - shelf_life_days: product.estimated_shelf_life_days || 30, - requires_refrigeration: product.requires_refrigeration || false, - requires_freezing: product.requires_freezing || false, - is_seasonal: product.is_seasonal || false, - minimum_stock_level: 0, - maximum_stock_level: 1000, - reorder_point: 10 - }; - - try { - // Use the onboarding hook's inventory creation method - const response = await createInventoryFromSuggestions([{ - suggestion_id: product.suggestion_id || `suggestion-${Date.now()}-${index}`, - original_name: product.original_name || product.name, - suggested_name: product.suggested_name || product.name, - product_type: product.product_type || 'finished_product', - category: product.category || 'general', - unit_of_measure: product.unit_of_measure || 'unit', - confidence_score: product.confidence_score || 0.8, - estimated_shelf_life_days: product.estimated_shelf_life_days || 30, - requires_refrigeration: product.requires_refrigeration || false, - requires_freezing: product.requires_freezing || false, - is_seasonal: product.is_seasonal || false, - suggested_supplier: product.suggested_supplier, - notes: product.notes - }]); - const success = !!response; - if (success) { - successCount++; - // Mock created item data since hook doesn't return it - const createdItem = { ...ingredientData, id: `created-${Date.now()}-${successCount}` }; - createdItems.push(createdItem); - inventoryMapping[product.original_name || product.name] = createdItem.id; - } else { - failCount++; - } - } catch (ingredientError) { - console.error('Error creating ingredient:', product.name, ingredientError); - failCount++; - // For onboarding, continue even if backend is not ready - // Mock success for onboarding flow - successCount++; - const createdItem = { ...ingredientData, id: `created-${Date.now()}-${successCount}` }; - createdItems.push(createdItem); - inventoryMapping[product.original_name || product.name] = createdItem.id; - } - } + // Approved products should already be in ProductSuggestionResponse format + // Just ensure they have all required fields + const suggestions = approvedProducts.map((product: any, index: number) => ({ + suggestion_id: product.suggestion_id || `suggestion-${Date.now()}-${index}`, + original_name: product.original_name, + suggested_name: product.suggested_name, + product_type: product.product_type, + category: product.category, + unit_of_measure: product.unit_of_measure, + confidence_score: product.confidence_score || 0.8, + estimated_shelf_life_days: product.estimated_shelf_life_days, + requires_refrigeration: product.requires_refrigeration, + requires_freezing: product.requires_freezing, + is_seasonal: product.is_seasonal, + suggested_supplier: product.suggested_supplier, + notes: product.notes + })); - // Show results - if (successCount > 0) { + // Use business onboarding hook to create inventory + const inventorySuccess = await createInventoryFromSuggestions(suggestions); + + if (inventorySuccess) { createAlert({ type: 'success', category: 'system', priority: 'medium', title: 'Inventario creado', - message: `Se crearon ${successCount} elementos de inventario exitosamente.`, + message: `Se crearon ${suggestions.length} elementos de inventario exitosamente.`, source: 'onboarding' }); - } else if (failCount > 0) { + + // Now try to import sales data if available + const salesDataFile = data.allStepData?.['data-processing']?.salesDataFile || + allStepData?.['data-processing']?.salesDataFile; + const processingResults = data.allStepData?.['data-processing']?.processingResults || + allStepData?.['data-processing']?.processingResults; + + if (salesDataFile && processingResults?.is_valid && inventoryMapping) { + try { + createAlert({ + type: 'info', + category: 'system', + priority: 'medium', + title: 'Subiendo datos de ventas', + message: 'Subiendo historial de ventas al sistema para entrenamiento de IA...', + source: 'onboarding' + }); + + const salesSuccess = await importSalesData(processingResults, inventoryMapping); + + if (salesSuccess) { + createAlert({ + type: 'success', + category: 'system', + priority: 'medium', + title: 'Datos de ventas subidos', + message: `Se subieron ${processingResults.total_records} registros de ventas al sistema exitosamente.`, + source: 'onboarding' + }); + } + } catch (salesError) { + console.error('Error uploading sales data:', salesError); + createAlert({ + type: 'error', + category: 'system', + priority: 'high', + title: 'Error al subir datos de ventas', + message: 'El inventario se creó correctamente, pero hubo un problema al subir los datos de ventas.', + source: 'onboarding' + }); + } + } + + // Update component data and business hook data + const updatedData = { + ...data, + inventoryItems: items, + inventoryConfigured: true, + inventoryCreated: true, + inventoryMapping, + createdInventoryItems: createdItems, + salesImportResult + }; + onDataChange(updatedData); + + // Also update step data in business hooks + updateStepData('inventory', { + inventoryItems: items, + inventoryConfigured: true, + inventoryCreated: true, + inventoryMapping, + createdInventoryItems: createdItems, + salesImportResult + }); + + } else { createAlert({ type: 'error', category: 'system', priority: 'high', title: 'Error al crear inventario', - message: `No se pudieron crear los elementos de inventario. Backend no disponible.`, + message: 'No se pudieron crear los elementos de inventario.', source: 'onboarding' }); - // Don't continue with sales import if inventory creation failed - return; } - // Now upload sales data to backend (required for ML training) - const salesDataFile = data.allStepData?.['data-processing']?.salesDataFile; - const processingResults = data.allStepData?.['data-processing']?.processingResults; - console.log('InventorySetup - salesDataFile:', salesDataFile); - console.log('InventorySetup - processingResults:', processingResults); - let salesImportResult = null; - - if (salesDataFile && processingResults?.is_valid) { - try { - createAlert({ - type: 'info', - category: 'system', - priority: 'medium', - title: 'Subiendo datos de ventas', - message: 'Subiendo historial de ventas al sistema para entrenamiento de IA...', - source: 'onboarding' - }); - - // TODO: Implement bulk sales record creation from file - // For now, simulate success - const importSuccess = true; - - if (importSuccess) { - salesImportResult = { - records_created: processingResults.total_records, - success: true, - imported: true - }; - - createAlert({ - type: 'success', - category: 'system', - priority: 'medium', - title: 'Datos de ventas subidos', - message: `Se subieron ${processingResults.total_records} registros de ventas al sistema exitosamente.`, - source: 'onboarding' - }); - } else { - throw new Error('Failed to upload sales data'); - } - } catch (salesError) { - console.error('Error uploading sales data:', salesError); - createAlert({ - type: 'error', - category: 'system', - priority: 'high', - title: 'Error al subir datos de ventas', - message: 'El inventario se creó correctamente, pero hubo un problema al subir los datos de ventas. Esto es requerido para el entrenamiento de IA.', - source: 'onboarding' - }); - - // Set failed result - salesImportResult = { - records_created: 0, - success: false, - error: salesError instanceof Error ? salesError.message : 'Error uploading sales data' - }; - } - } - - // Update the step data with created inventory and sales import result - console.log('InventorySetup - Updating step data with salesImportResult:', salesImportResult); - const updatedData = { - ...data, - inventoryItems: items, - inventoryConfigured: true, - inventoryCreated: true, // Mark as created to prevent duplicate calls - inventoryMapping: inventoryMapping, - createdInventoryItems: createdItems, - salesImportResult: salesImportResult - }; - console.log('InventorySetup - updatedData:', updatedData); - onDataChange(updatedData); - } catch (error) { console.error('Error creating inventory:', error); const errorMessage = error instanceof Error ? error.message : 'Error al crear inventario'; @@ -339,17 +306,34 @@ export const InventorySetupStep: React.FC = ({ } }; + const lastItemsRef = useRef(items); + const lastIsCreatingRef = useRef(isCreating); + useEffect(() => { - const hasValidStock = items.length > 0 && items.every(item => - item.min_stock >= 0 && item.max_stock > item.min_stock - ); - - onDataChange({ - ...data, - inventoryItems: items, - inventoryConfigured: hasValidStock && !isCreating - }); - }, [items, isCreating]); + // Only update if items or isCreating actually changed + if (JSON.stringify(items) !== JSON.stringify(lastItemsRef.current) || isCreating !== lastIsCreatingRef.current) { + lastItemsRef.current = items; + lastIsCreatingRef.current = isCreating; + + const hasValidStock = items.length > 0 && items.every(item => + item.min_stock >= 0 && item.max_stock > item.min_stock + ); + + const stepData = { + inventoryItems: items, + inventoryConfigured: hasValidStock && !isCreating + }; + + // Update component props + onDataChange({ + ...data, + ...stepData + }); + + // Update business hooks data + updateStepData('inventory', stepData); + } + }, [items, isCreating]); // Only depend on items and isCreating // Auto-create inventory when step is completed (when user clicks Next) useEffect(() => { @@ -358,11 +342,12 @@ export const InventorySetupStep: React.FC = ({ ); // If inventory is configured but not yet created in backend, create it automatically - if (hasValidStock && !data.inventoryCreated && !isCreating) { + if (hasValidStock && !data.inventoryCreated && !isCreating && !inventoryCreationAttempted) { console.log('InventorySetup - Auto-creating inventory on step completion'); + setInventoryCreationAttempted(true); handleCreateInventory(); } - }, [data.inventoryCreated, items, isCreating]); + }, [data.inventoryCreated, items, isCreating, inventoryCreationAttempted]); const handleAddItem = () => { const newItem: InventoryItem = { diff --git a/frontend/src/components/domain/onboarding/steps/ReviewStep.tsx b/frontend/src/components/domain/onboarding/steps/ReviewStep.tsx index 6b6a1981..c4b60c49 100644 --- a/frontend/src/components/domain/onboarding/steps/ReviewStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/ReviewStep.tsx @@ -168,14 +168,13 @@ export const ReviewStep: React.FC = ({ approvedProductsCount: approvedProducts.length }); onDataChange({ - ...data, detectedProducts: products, approvedProducts, reviewCompleted }); dataChangeRef.current = currentState; } - }, [products, approvedProducts, reviewCompleted]); + }, [products, approvedProducts, reviewCompleted, onDataChange]); // Handle review completion alert separately useEffect(() => { diff --git a/frontend/src/hooks/business/onboarding/useInventorySetup.ts b/frontend/src/hooks/business/onboarding/useInventorySetup.ts index 0f931b86..ad79c7f9 100644 --- a/frontend/src/hooks/business/onboarding/useInventorySetup.ts +++ b/frontend/src/hooks/business/onboarding/useInventorySetup.ts @@ -65,7 +65,14 @@ export const useInventorySetup = () => { createdItems?: any[]; inventoryMapping?: { [productName: string]: string }; }> => { + console.log('useInventorySetup - createInventoryFromSuggestions called with:', { + suggestionsCount: suggestions?.length, + suggestions: suggestions?.slice(0, 3), // Log first 3 for debugging + tenantId: currentTenant?.id + }); + if (!suggestions || suggestions.length === 0) { + console.error('useInventorySetup - No suggestions provided'); setState(prev => ({ ...prev, error: 'No hay sugerencias para crear el inventario', @@ -74,6 +81,7 @@ export const useInventorySetup = () => { } if (!currentTenant?.id) { + console.error('useInventorySetup - No tenant ID available'); setState(prev => ({ ...prev, error: 'No se pudo obtener información del tenant', @@ -91,31 +99,54 @@ export const useInventorySetup = () => { const createdItems = []; const inventoryMapping: { [key: string]: string } = {}; + console.log('useInventorySetup - Creating ingredients from suggestions...'); + // Create ingredients from approved suggestions for (const suggestion of suggestions) { try { const ingredientData = { - name: suggestion.name, + name: suggestion.suggested_name || suggestion.original_name, category: suggestion.category || 'Sin categoría', - description: suggestion.description || '', + description: suggestion.notes || '', unit_of_measure: suggestion.unit_of_measure || 'unidad', - cost_per_unit: suggestion.cost_per_unit || 0, - supplier_info: suggestion.supplier_info || {}, - nutritional_info: suggestion.nutritional_info || {}, - storage_requirements: suggestion.storage_requirements || {}, - allergen_info: suggestion.allergen_info || {}, - is_active: true, + minimum_stock_level: 1, // Default minimum stock + maximum_stock_level: 100, // Default maximum stock + reorder_point: 5, // Default reorder point + shelf_life_days: suggestion.estimated_shelf_life_days || 30, + requires_refrigeration: suggestion.requires_refrigeration || false, + requires_freezing: suggestion.requires_freezing || false, + is_seasonal: suggestion.is_seasonal || false, + cost_per_unit: 0, // Will be set by user later + notes: suggestion.notes || `Producto clasificado automáticamente desde: ${suggestion.original_name}`, }; + console.log('useInventorySetup - Creating ingredient:', { + name: ingredientData.name, + category: ingredientData.category, + original_name: suggestion.original_name, + ingredientData: ingredientData, + tenantId: currentTenant.id, + apiUrl: `/tenants/${currentTenant.id}/ingredients` + }); + const createdItem = await createIngredientMutation.mutateAsync({ tenantId: currentTenant.id, ingredientData, }); + console.log('useInventorySetup - Created ingredient successfully:', { + id: createdItem.id, + name: createdItem.name + }); + createdItems.push(createdItem); - inventoryMapping[suggestion.name] = createdItem.id; + // Map both original and suggested names to the same ingredient ID for flexibility + inventoryMapping[suggestion.original_name] = createdItem.id; + if (suggestion.suggested_name && suggestion.suggested_name !== suggestion.original_name) { + inventoryMapping[suggestion.suggested_name] = createdItem.id; + } } catch (error) { - console.error(`Error creating ingredient ${suggestion.name}:`, error); + console.error(`Error creating ingredient ${suggestion.suggested_name || suggestion.original_name}:`, error); // Continue with other ingredients even if one fails } } @@ -123,6 +154,12 @@ export const useInventorySetup = () => { if (createdItems.length === 0) { throw new Error('No se pudo crear ningún elemento del inventario'); } + + console.log('useInventorySetup - Successfully created ingredients:', { + createdCount: createdItems.length, + totalSuggestions: suggestions.length, + inventoryMapping + }); setState(prev => ({ ...prev, @@ -139,6 +176,7 @@ export const useInventorySetup = () => { }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Error creando el inventario'; + console.error('useInventorySetup - Error in createInventoryFromSuggestions:', error); setState(prev => ({ ...prev, isLoading: false, diff --git a/frontend/src/hooks/business/onboarding/useOnboarding.ts b/frontend/src/hooks/business/onboarding/useOnboarding.ts index 4a71a195..4b9197f6 100644 --- a/frontend/src/hooks/business/onboarding/useOnboarding.ts +++ b/frontend/src/hooks/business/onboarding/useOnboarding.ts @@ -20,35 +20,6 @@ import type { } from './types'; import type { BakeryRegistration } from '../../../api'; -interface OnboardingActions { - // Navigation - nextStep: () => boolean; - previousStep: () => boolean; - goToStep: (stepIndex: number) => boolean; - - // Data Management - updateStepData: (stepId: string, data: Partial) => void; - validateCurrentStep: () => string | null; - - // Step-specific Actions - createTenant: (bakeryData: BakeryRegistration) => Promise; - processSalesFile: (file: File, onProgress?: ProgressCallback) => Promise; - generateInventorySuggestions: (productList: string[]) => Promise; - createInventoryFromSuggestions: (suggestions: ProductSuggestionResponse[]) => Promise; - importSalesData: (salesData: any, inventoryMapping: { [productName: string]: string }) => Promise; - startTraining: (options?: { - products?: string[]; - startDate?: string; - endDate?: string; - }) => Promise; - - // Completion - completeOnboarding: () => Promise; - - // Utilities - clearError: () => void; - reset: () => void; -} export const useOnboarding = () => { const navigate = useNavigate(); @@ -63,28 +34,6 @@ export const useOnboarding = () => { const inventorySetup = useInventorySetup(); const trainingOrchestration = useTrainingOrchestration(); - // Navigation actions - const nextStep = useCallback((): boolean => { - const validation = validateCurrentStep(); - if (validation) { - return false; - } - - if (flow.nextStep()) { - flow.markStepCompleted(flow.currentStep - 1); - return true; - } - return false; - }, [flow]); - - const previousStep = useCallback((): boolean => { - return flow.previousStep(); - }, [flow]); - - const goToStep = useCallback((stepIndex: number): boolean => { - return flow.goToStep(stepIndex); - }, [flow]); - // Data management const updateStepData = useCallback((stepId: string, stepData: Partial) => { data.updateStepData(stepId, stepData); @@ -104,10 +53,69 @@ export const useOnboarding = () => { const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise => { const success = await tenantCreation.createTenant(bakeryData); if (success) { - updateStepData('setup', { bakery: bakeryData }); + // Store the bakery data with tenant creation success flag and tenant ID + updateStepData('setup', { + bakery: { + ...bakeryData, + tenantCreated: true, + tenant_id: currentTenant?.id || 'created' + } as any // Type assertion to allow the additional properties + }); + console.log('useOnboarding - Tenant created successfully, updated step data with tenant ID:', currentTenant?.id); } return success; - }, [tenantCreation, updateStepData]); + }, [tenantCreation, updateStepData, currentTenant]); + + // Navigation actions + const nextStep = useCallback(async (): Promise => { + try { + const currentStep = flow.getCurrentStep(); + console.log('useOnboarding - nextStep called from step:', currentStep.id); + + const validation = validateCurrentStep(); + if (validation) { + console.log('useOnboarding - Validation failed:', validation); + return false; + } + + // Handle step-specific actions before moving to next step + if (currentStep.id === 'setup') { + console.log('useOnboarding - Creating tenant before leaving setup step'); + const allStepData = data.getAllStepData(); + const bakeryData = allStepData?.setup?.bakery; + + if (bakeryData && !tenantCreation.isSuccess) { + console.log('useOnboarding - Tenant data found, creating tenant:', bakeryData); + const tenantSuccess = await createTenant(bakeryData); + console.log('useOnboarding - Tenant creation result:', tenantSuccess); + + if (!tenantSuccess) { + console.log('useOnboarding - Tenant creation failed, stopping navigation'); + return false; + } + } else { + console.log('useOnboarding - No tenant data found or tenant already created'); + } + } + + if (flow.nextStep()) { + flow.markStepCompleted(flow.currentStep - 1); + return true; + } + return false; + } catch (error) { + console.error('useOnboarding - Error in nextStep:', error); + return false; + } + }, [flow, validateCurrentStep, data, createTenant, tenantCreation]); + + const previousStep = useCallback((): boolean => { + return flow.previousStep(); + }, [flow]); + + const goToStep = useCallback((stepIndex: number): boolean => { + return flow.goToStep(stepIndex); + }, [flow]); const processSalesFile = useCallback(async ( file: File, @@ -299,8 +307,5 @@ export const useOnboarding = () => { completeOnboarding, clearError, reset, - } satisfies ReturnType & - ReturnType & - { [key: string]: any } & - OnboardingActions; + }; }; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/useSalesProcessing.ts b/frontend/src/hooks/business/onboarding/useSalesProcessing.ts index 80a737b6..1369947f 100644 --- a/frontend/src/hooks/business/onboarding/useSalesProcessing.ts +++ b/frontend/src/hooks/business/onboarding/useSalesProcessing.ts @@ -3,7 +3,7 @@ */ import { useState, useCallback } from 'react'; -import { useClassifyProductsBatch, useValidateAndImportFile } from '../../../api'; +import { useClassifyProductsBatch, useValidateFileOnly } from '../../../api'; import { useCurrentTenant } from '../../../stores'; import type { ProductSuggestionResponse, ProgressCallback } from './types'; @@ -41,7 +41,7 @@ export const useSalesProcessing = () => { const classifyProductsMutation = useClassifyProductsBatch(); const currentTenant = useCurrentTenant(); - const { processFile: processFileImport } = useValidateAndImportFile(); + const { validateFile } = useValidateFileOnly(); const updateProgress = useCallback((progress: number, stage: string, message: string, onProgress?: ProgressCallback) => { setState(prev => ({ @@ -76,21 +76,35 @@ export const useSalesProcessing = () => { updateProgress(20, 'validating', 'Validando estructura del archivo...', onProgress); // Use the actual data import hook for file validation + console.log('useSalesProcessing - currentTenant check:', { + currentTenant, + tenantId: currentTenant?.id, + hasTenant: !!currentTenant, + hasId: !!currentTenant?.id + }); + if (!currentTenant?.id) { - throw new Error('No se pudo obtener información del tenant'); + throw new Error(`No se pudo obtener información del tenant. Tenant: ${JSON.stringify(currentTenant)}`); } - const result = await processFileImport( + const result = await validateFile( currentTenant.id, file, { - skipValidation: false, onProgress: (stage, progress, message) => { updateProgress(progress, stage, message, onProgress); } } ); + console.log('useSalesProcessing - Backend result:', { + success: result.success, + hasValidationResult: !!result.validationResult, + validationResult: result.validationResult, + error: result.error, + fullResult: result + }); + if (!result.success || !result.validationResult) { throw new Error(result.error || 'Error en la validación del archivo'); } @@ -100,9 +114,21 @@ export const useSalesProcessing = () => { product_list: extractProductList(result.validationResult), }; + console.log('useSalesProcessing - Processed validation result:', { + validationResult, + hasProductList: !!validationResult.product_list, + productListLength: validationResult.product_list?.length || 0, + productListSample: validationResult.product_list?.slice(0, 5) + }); + updateProgress(40, 'validating', 'Verificando integridad de datos...', onProgress); if (!validationResult || !validationResult.product_list || validationResult.product_list.length === 0) { + console.error('useSalesProcessing - No products found:', { + hasValidationResult: !!validationResult, + hasProductList: !!validationResult?.product_list, + productListLength: validationResult?.product_list?.length + }); throw new Error('No se encontraron productos válidos en el archivo'); } @@ -144,11 +170,24 @@ export const useSalesProcessing = () => { success: false, }; } - }, [updateProgress, currentTenant, processFileImport]); + }, [updateProgress, currentTenant, validateFile]); // Helper to extract product list from validation result const extractProductList = useCallback((validationResult: any): string[] => { - // Extract unique product names from sample records + console.log('extractProductList - Input validation result:', { + hasProductList: !!validationResult?.product_list, + productList: validationResult?.product_list, + hasSampleRecords: !!validationResult?.sample_records, + keys: Object.keys(validationResult || {}) + }); + + // First try to use the direct product_list from backend response + if (validationResult.product_list && Array.isArray(validationResult.product_list)) { + console.log('extractProductList - Using direct product_list:', validationResult.product_list); + return validationResult.product_list; + } + + // Fallback: Extract unique product names from sample records if (validationResult.sample_records && Array.isArray(validationResult.sample_records)) { const productSet = new Set(); validationResult.sample_records.forEach((record: any) => { @@ -156,8 +195,12 @@ export const useSalesProcessing = () => { productSet.add(record.product_name); } }); - return Array.from(productSet); + const extractedList = Array.from(productSet); + console.log('extractProductList - Extracted from sample_records:', extractedList); + return extractedList; } + + console.log('extractProductList - No products found, returning empty array'); return []; }, []); diff --git a/frontend/src/hooks/business/onboarding/useTenantCreation.ts b/frontend/src/hooks/business/onboarding/useTenantCreation.ts index 6bd25ef3..e1c19c19 100644 --- a/frontend/src/hooks/business/onboarding/useTenantCreation.ts +++ b/frontend/src/hooks/business/onboarding/useTenantCreation.ts @@ -4,7 +4,8 @@ import { useState, useCallback } from 'react'; import { useRegisterBakery } from '../../../api'; -import type { BakeryRegistration } from '../../../api'; +import type { BakeryRegistration, TenantResponse } from '../../../api'; +import { useTenantStore } from '../../../stores/tenant.store'; interface TenantCreationState { isLoading: boolean; @@ -28,6 +29,7 @@ export const useTenantCreation = () => { }); const registerBakeryMutation = useRegisterBakery(); + const { setCurrentTenant, loadUserTenants } = useTenantStore(); const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise => { if (!bakeryData) { @@ -46,7 +48,14 @@ export const useTenantCreation = () => { })); try { - await registerBakeryMutation.mutateAsync(bakeryData); + const tenantResponse: TenantResponse = await registerBakeryMutation.mutateAsync(bakeryData); + + // Update the tenant store with the newly created tenant + console.log('useTenantCreation - Setting current tenant:', tenantResponse); + setCurrentTenant(tenantResponse); + + // Reload user tenants to ensure the list is up to date + await loadUserTenants(); setState(prev => ({ ...prev, @@ -55,6 +64,7 @@ export const useTenantCreation = () => { tenantData: bakeryData, })); + console.log('useTenantCreation - Tenant created and set successfully'); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Error al crear la panadería'; diff --git a/frontend/src/pages/app/onboarding/OnboardingPage.tsx b/frontend/src/pages/app/onboarding/OnboardingPage.tsx index bb061324..9c3c847c 100644 --- a/frontend/src/pages/app/onboarding/OnboardingPage.tsx +++ b/frontend/src/pages/app/onboarding/OnboardingPage.tsx @@ -68,19 +68,25 @@ const OnboardingPage: React.FC = () => { } }; - const handleNext = () => { - return nextStep(); + const handleNext = async (): Promise => { + try { + const success = await nextStep(); + return success; + } catch (error) { + console.error('Error in handleNext:', error); + return false; + } }; - const handlePrevious = () => { + const handlePrevious = (): boolean => { return previousStep(); }; - const handleComplete = async (allData: any) => { - const success = await completeOnboarding(); - if (success) { - // Navigation is handled inside completeOnboarding - return; + const handleComplete = async (allData: any): Promise => { + try { + await completeOnboarding(); + } catch (error) { + console.error('Error in handleComplete:', error); } }; diff --git a/frontend/src/stores/auth.store.ts b/frontend/src/stores/auth.store.ts index 10a93fa2..e6fed9ca 100644 --- a/frontend/src/stores/auth.store.ts +++ b/frontend/src/stores/auth.store.ts @@ -40,7 +40,7 @@ export interface AuthState { canAccess: (resource: string, action: string) => boolean; } -import { authService } from '../api'; +import { authService, apiClient } from '../api'; export const useAuthStore = create()( persist( @@ -61,6 +61,9 @@ export const useAuthStore = create()( const response = await authService.login({ email, password }); if (response && response.access_token) { + // Set the auth token on the API client immediately + apiClient.setAuthToken(response.access_token); + set({ user: response.user || null, token: response.access_token, @@ -92,6 +95,9 @@ export const useAuthStore = create()( const response = await authService.register(userData); if (response && response.access_token) { + // Set the auth token on the API client immediately + apiClient.setAuthToken(response.access_token); + set({ user: response.user || null, token: response.access_token, @@ -117,6 +123,10 @@ export const useAuthStore = create()( }, logout: () => { + // Clear the auth token from API client + apiClient.setAuthToken(null); + apiClient.setTenantId(null); + set({ user: null, token: null, @@ -139,6 +149,9 @@ export const useAuthStore = create()( const response = await authService.refreshToken(refreshToken); if (response && response.access_token) { + // Set the auth token on the API client immediately + apiClient.setAuthToken(response.access_token); + set({ token: response.access_token, refreshToken: response.refresh_token || refreshToken,