Start integrating the onboarding flow with backend 10

This commit is contained in:
Urtzi Alfaro
2025-09-06 19:40:47 +02:00
parent 905f848573
commit d2083856fa
16 changed files with 768 additions and 315 deletions

View File

@@ -108,6 +108,72 @@ export const useImportCsvFile = (
}; };
// Combined validation and import hook for easier use // 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 = () => { export const useValidateAndImportFile = () => {
const validateCsv = useValidateCsvFile(); const validateCsv = useValidateCsvFile();
const validateJson = useValidateJsonData(); const validateJson = useValidateJsonData();
@@ -147,9 +213,17 @@ export const useValidateAndImportFile = () => {
options?.onProgress?.('validating', 50, 'Verificando integridad de datos...'); options?.onProgress?.('validating', 50, 'Verificando integridad de datos...');
if (!validationResult.valid) { if (!validationResult.is_valid) {
throw new Error(`Archivo inválido: ${validationResult.errors?.join(', ')}`); 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 // Step 2: Import
@@ -180,12 +254,18 @@ export const useValidateAndImportFile = () => {
throw new Error('Formato de archivo no soportado. Use CSV o JSON.'); 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 { return {
validationResult, validationResult,
importResult, importResult,
success: true, success: importResult.success,
error: importResult.success ? undefined : (importResult.errors?.join(', ') || 'Error en la importación'),
}; };
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error procesando archivo'; const errorMessage = error instanceof Error ? error.message : 'Error procesando archivo';

View File

@@ -384,6 +384,7 @@ export {
useValidateCsvFile, useValidateCsvFile,
useImportJsonData, useImportJsonData,
useImportCsvFile, useImportCsvFile,
useValidateFileOnly,
useValidateAndImportFile, useValidateAndImportFile,
dataImportKeys, dataImportKeys,
} from './hooks/dataImport'; } from './hooks/dataImport';

View File

@@ -27,9 +27,12 @@ export class DataImportService {
tenantId: string, tenantId: string,
file: File file: File
): Promise<ImportValidationResponse> { ): Promise<ImportValidationResponse> {
const formData = new FormData();
formData.append('file', file);
return apiClient.uploadFile<ImportValidationResponse>( return apiClient.uploadFile<ImportValidationResponse>(
`${this.baseUrl}/${tenantId}/sales/import/validate-csv`, `${this.baseUrl}/${tenantId}/sales/import/validate-csv`,
file formData
); );
} }

View File

@@ -5,14 +5,16 @@ import { apiClient } from '../client';
import { UserProgress, UpdateStepRequest } from '../types/onboarding'; import { UserProgress, UpdateStepRequest } from '../types/onboarding';
export class OnboardingService { export class OnboardingService {
private readonly baseUrl = '/onboarding'; private readonly baseUrl = '/users/me/onboarding';
async getUserProgress(userId: string): Promise<UserProgress> { async getUserProgress(userId: string): Promise<UserProgress> {
return apiClient.get<UserProgress>(`${this.baseUrl}/progress/${userId}`); // Backend uses current user from auth token, so userId parameter is ignored
return apiClient.get<UserProgress>(`${this.baseUrl}/progress`);
} }
async updateStep(userId: string, stepData: UpdateStepRequest): Promise<UserProgress> { async updateStep(userId: string, stepData: UpdateStepRequest): Promise<UserProgress> {
return apiClient.put<UserProgress>(`${this.baseUrl}/progress/${userId}/step`, stepData); // Backend uses current user from auth token, so userId parameter is ignored
return apiClient.put<UserProgress>(`${this.baseUrl}/step`, stepData);
} }
async markStepCompleted( async markStepCompleted(
@@ -20,14 +22,19 @@ export class OnboardingService {
stepName: string, stepName: string,
data?: Record<string, any> data?: Record<string, any>
): Promise<UserProgress> { ): Promise<UserProgress> {
return apiClient.post<UserProgress>(`${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<UserProgress>(`${this.baseUrl}/step`, {
step_name: stepName, step_name: stepName,
completed: true,
data: data, data: data,
}); });
} }
async resetProgress(userId: string): Promise<UserProgress> { async resetProgress(userId: string): Promise<UserProgress> {
return apiClient.post<UserProgress>(`${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<{ async getStepDetails(stepName: string): Promise<{
@@ -36,7 +43,8 @@ export class OnboardingService {
dependencies: string[]; dependencies: string[];
estimated_time_minutes: number; 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<Array<{ async getAllSteps(): Promise<Array<{
@@ -45,7 +53,23 @@ export class OnboardingService {
dependencies: string[]; dependencies: string[];
estimated_time_minutes: number; estimated_time_minutes: number;
}>> { }>> {
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`);
} }
} }

View File

@@ -9,11 +9,30 @@ export interface ImportValidationRequest {
} }
export interface ImportValidationResponse { export interface ImportValidationResponse {
valid: boolean; is_valid: boolean;
total_records: number;
valid_records: number;
invalid_records: number;
errors: string[]; errors: string[];
warnings: string[]; warnings: string[];
record_count?: number; summary: {
sample_records?: any[]; 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 { export interface ImportProcessRequest {
@@ -33,6 +52,18 @@ export interface ImportProcessResponse {
records_failed: number; records_failed: number;
import_id?: string; import_id?: string;
errors?: 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 { export interface ImportStatusResponse {

View File

@@ -26,10 +26,10 @@ interface OnboardingWizardProps {
currentStep: number; currentStep: number;
data: any; data: any;
onStepChange: (stepIndex: number, stepData: any) => void; onStepChange: (stepIndex: number, stepData: any) => void;
onNext: () => void; onNext: () => Promise<boolean> | boolean;
onPrevious: () => void; onPrevious: () => boolean;
onComplete: (data: any) => void; onComplete: (data: any) => Promise<void> | void;
onGoToStep: (stepIndex: number) => void; onGoToStep: (stepIndex: number) => boolean;
onExit?: () => void; onExit?: () => void;
className?: string; className?: string;
} }
@@ -82,9 +82,12 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
return true; return true;
}, [currentStep, stepData]); }, [currentStep, stepData]);
const goToNextStep = useCallback(() => { const goToNextStep = useCallback(async () => {
if (validateCurrentStep()) { if (validateCurrentStep()) {
onNext(); const result = onNext();
if (result instanceof Promise) {
await result;
}
} }
}, [validateCurrentStep, onNext]); }, [validateCurrentStep, onNext]);
@@ -356,7 +359,13 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
<Button <Button
variant="primary" variant="primary"
onClick={goToNextStep} onClick={async () => {
if (currentStepIndex === steps.length - 1) {
await onComplete(stepData);
} else {
await goToNextStep();
}
}}
disabled={ disabled={
(currentStep.validation && currentStep.validation(stepData || {})) (currentStep.validation && currentStep.validation(stepData || {}))
} }

View File

@@ -1,7 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Store, MapPin, Phone, Mail, Hash, Building } from 'lucide-react'; import { Store, MapPin, Phone, Mail, Hash, Building } from 'lucide-react';
import { Button, Card, Input } from '../../../ui'; import { Button, Card, Input } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard'; import { OnboardingStepProps } from '../OnboardingWizard';
import { useAuthUser } from '../../../../stores/auth.store';
import { useOnboarding } from '../../../../hooks/business/onboarding';
// Backend-compatible bakery setup interface // Backend-compatible bakery setup interface
interface BakerySetupData { interface BakerySetupData {
@@ -24,6 +26,18 @@ export const BakerySetupStep: React.FC<OnboardingStepProps> = ({
isFirstStep, isFirstStep,
isLastStep isLastStep
}) => { }) => {
const user = useAuthUser();
const userId = user?.id;
// Business onboarding hooks
const {
updateStepData,
tenantCreation,
isLoading,
error,
clearError
} = useOnboarding();
const [formData, setFormData] = useState<BakerySetupData>({ const [formData, setFormData] = useState<BakerySetupData>({
name: data.bakery?.name || '', name: data.bakery?.name || '',
business_type: data.bakery?.business_type || 'bakery', business_type: data.bakery?.business_type || 'bakery',
@@ -63,15 +77,33 @@ export const BakerySetupStep: React.FC<OnboardingStepProps> = ({
} }
]; ];
const lastFormDataRef = useRef(formData);
useEffect(() => { useEffect(() => {
// Update parent data when form changes // Only update if formData actually changed and is valid
onDataChange({ if (JSON.stringify(formData) !== JSON.stringify(lastFormDataRef.current)) {
bakery: { lastFormDataRef.current = formData;
// Update parent data when form changes
const bakeryData = {
...formData, ...formData,
tenant_id: data.bakery?.tenant_id tenant_id: data.bakery?.tenant_id
};
onDataChange({ bakery: bakeryData });
}
}, [formData, onDataChange, data.bakery?.tenant_id]);
// Separate effect for hook updates to avoid circular dependencies
useEffect(() => {
const timeoutId = setTimeout(() => {
if (userId && Object.values(formData).some(value => value.trim() !== '')) {
updateStepData('setup', { bakery: formData });
} }
}); }, 1000);
}, [formData]);
return () => clearTimeout(timeoutId);
}, [formData, userId, updateStepData]);
const handleInputChange = (field: keyof BakerySetupData, value: string) => { const handleInputChange = (field: keyof BakerySetupData, value: string) => {
setFormData(prev => ({ setFormData(prev => ({
@@ -81,6 +113,21 @@ export const BakerySetupStep: React.FC<OnboardingStepProps> = ({
}; };
// Validate form data for completion
const isFormValid = () => {
return !!(
formData.name &&
formData.address &&
formData.city &&
formData.postal_code &&
formData.phone &&
formData.business_type &&
formData.business_model
);
};
return ( return (
<div className="space-y-8"> <div className="space-y-8">
@@ -227,6 +274,16 @@ export const BakerySetupStep: React.FC<OnboardingStepProps> = ({
</div> </div>
</div> </div>
{/* Show loading state for tenant creation */}
{tenantCreation.isLoading && (
<div className="text-center p-4 bg-blue-50 rounded-lg">
<div className="animate-spin w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full mx-auto mb-2" />
<p className="text-blue-600 text-sm">
Configurando panadería...
</p>
</div>
)}
{/* Show loading state when creating tenant */} {/* Show loading state when creating tenant */}
{data.bakery?.isCreating && ( {data.bakery?.isCreating && (
<div className="text-center p-6 bg-[var(--color-primary)]/5 rounded-lg"> <div className="text-center p-6 bg-[var(--color-primary)]/5 rounded-lg">
@@ -237,6 +294,24 @@ export const BakerySetupStep: React.FC<OnboardingStepProps> = ({
</div> </div>
)} )}
{/* Show error state for business onboarding operations */}
{error && (
<div className="text-center p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600 font-medium mb-2">
Error al configurar panadería
</p>
<p className="text-red-500 text-sm">
{error}
</p>
<button
onClick={clearError}
className="mt-2 text-sm text-red-600 underline hover:no-underline"
>
Ocultar error
</button>
</div>
)}
{/* Show error state if tenant creation fails */} {/* Show error state if tenant creation fails */}
{data.bakery?.creationError && ( {data.bakery?.creationError && (
<div className="text-center p-6 bg-red-50 border border-red-200 rounded-lg"> <div className="text-center p-6 bg-red-50 border border-red-200 rounded-lg">
@@ -248,6 +323,7 @@ export const BakerySetupStep: React.FC<OnboardingStepProps> = ({
</p> </p>
</div> </div>
)} )}
</div> </div>
); );
}; };

View File

@@ -57,6 +57,7 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
validationResults, validationResults,
suggestions suggestions
}, },
tenantCreation,
isLoading, isLoading,
error, error,
clearError clearError
@@ -70,22 +71,44 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
// Get tenant ID from multiple sources with fallback // Get tenant ID from multiple sources with fallback
const getTenantId = (): string | null => { 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:', { console.log('DataProcessingStep - getTenantId:', {
currentTenant: currentTenant?.id, currentTenant: currentTenant?.id,
userTenantId: user?.tenant_id, userTenantId: user?.tenant_id,
onboardingTenantId: onboardingTenantId,
finalTenantId: tenantId, finalTenantId: tenantId,
isLoadingUserData, isLoadingUserData,
authLoading, authLoading,
tenantLoading, 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; 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 => { 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 // Use onboarding hook state when available, fallback to local state
const [localStage, setLocalStage] = useState<ProcessingStage>(data.processingStage || 'upload'); const [localStage, setLocalStage] = useState<ProcessingStage>(data.processingStage || 'upload');
@@ -93,38 +116,67 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
const [localResults, setLocalResults] = useState<ProcessingResult | null>(data.processingResults || null); const [localResults, setLocalResults] = useState<ProcessingResult | null>(data.processingResults || null);
// Derive current state from onboarding hooks or local state // 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 progress = onboardingProgress || 0;
const currentMessage = onboardingMessage || ''; const currentMessage = onboardingMessage || '';
const results = (validationResults && suggestions) ? { const results = (validationResults && suggestions) ? {
...validationResults, ...validationResults,
aiSuggestions: suggestions, aiSuggestions: suggestions,
// Add calculated fields // Add calculated fields from backend response
productsIdentified: validationResults.product_list?.length || 0, productsIdentified: validationResults.unique_products || validationResults.product_list?.length || 0,
categoriesDetected: suggestions ? new Set(suggestions.map((s: any) => s.category)).size : 0, categoriesDetected: suggestions ? new Set(suggestions.map((s: any) => s.category)).size : 0,
businessModel: 'production', businessModel: 'production',
confidenceScore: 85, 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; } : 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 [dragActive, setDragActive] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const lastStateRef = useRef({ stage, progress, currentMessage, results, suggestions, uploadedFile });
useEffect(() => { useEffect(() => {
// Update parent data when state changes // Only update if state actually changed
onDataChange({ const currentState = { stage, progress, currentMessage, results, suggestions, uploadedFile };
...data, if (JSON.stringify(currentState) !== JSON.stringify(lastStateRef.current)) {
processingStage: stage, lastStateRef.current = currentState;
processingProgress: progress,
currentMessage: currentMessage, // Update parent data when state changes
processingResults: results, onDataChange({
suggestions: suggestions, processingStage: stage,
files: { processingProgress: progress,
...data.files, currentMessage: currentMessage,
salesData: uploadedFile processingResults: results,
} suggestions: suggestions,
}); files: {
}, [stage, progress, currentMessage, results, suggestions, uploadedFile, onDataChange, data]); ...data.files,
salesData: uploadedFile
}
});
}
}, [stage, progress, currentMessage, results, suggestions, uploadedFile, onDataChange, data.files]);
const handleDragOver = (e: React.DragEvent) => { const handleDragOver = (e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
@@ -190,7 +242,12 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
return; 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 // Use the onboarding hook for file processing
const success = await processSalesFile(file, (progress, stage, message) => { const success = await processSalesFile(file, (progress, stage, message) => {
@@ -199,6 +256,21 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
if (success) { if (success) {
setLocalStage('completed'); 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', { toast.addToast('El archivo se procesó correctamente', {
title: 'Procesamiento completado', title: 'Procesamiento completado',
type: 'success' type: 'success'
@@ -209,6 +281,15 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
} catch (error) { } catch (error) {
console.error('DataProcessingStep - Processing error:', 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'); setLocalStage('error');
const errorMessage = error instanceof Error ? error.message : 'Error en el procesamiento de datos'; const errorMessage = error instanceof Error ? error.message : 'Error en el procesamiento de datos';
@@ -471,15 +552,29 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
¡Procesamiento Completado! ¡Procesamiento Completado!
</h3> </h3>
<p className="text-[var(--text-secondary)] max-w-2xl mx-auto"> <p className="text-[var(--text-secondary)] max-w-2xl mx-auto">
Tus datos han sido procesados exitosamente {results.validationMessage || 'Tus datos han sido procesados exitosamente'}
</p> </p>
{results.fileSizeMb && (
<div className="text-xs text-[var(--text-tertiary)] mt-2">
Archivo {results.fileFormat?.toUpperCase()} {results.fileSizeMb.toFixed(2)} MB
{results.estimatedProcessingTime && `${results.estimatedProcessingTime}s procesamiento`}
</div>
)}
</div> </div>
{/* Simple Stats Cards */} {/* Enhanced Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-[var(--color-info)]/10 rounded-lg"> <div className="text-center p-4 bg-[var(--color-info)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-info)]">{results.total_records}</p> <p className="text-2xl font-bold text-[var(--color-info)]">{results.totalRecords || results.total_records}</p>
<p className="text-sm text-[var(--text-secondary)]">Registros</p> <p className="text-sm text-[var(--text-secondary)]">Total Registros</p>
</div>
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-success)]">{results.validRecords || results.valid_records}</p>
<p className="text-sm text-[var(--text-secondary)]">Válidos</p>
</div>
<div className="text-center p-4 bg-[var(--color-error)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-error)]">{results.invalidRecords || results.invalid_records || 0}</p>
<p className="text-sm text-[var(--text-secondary)]">Inválidos</p>
</div> </div>
<div className="text-center p-4 bg-[var(--color-primary)]/10 rounded-lg"> <div className="text-center p-4 bg-[var(--color-primary)]/10 rounded-lg">
@@ -500,6 +595,41 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
<p className="text-sm text-[var(--text-secondary)]">Modelo</p> <p className="text-sm text-[var(--text-secondary)]">Modelo</p>
</div> </div>
</div> </div>
{/* Additional Details from Backend */}
{(results.detectedColumns?.length > 0 || results.recommendations?.length > 0) && (
<div className="grid md:grid-cols-2 gap-6">
{/* Detected Columns */}
{results.detectedColumns?.length > 0 && (
<Card className="p-4">
<h4 className="font-semibold text-[var(--text-primary)] mb-3">Columnas Detectadas</h4>
<div className="flex flex-wrap gap-2">
{results.detectedColumns.map((column, index) => (
<span key={index}
className="px-2 py-1 bg-[var(--color-primary)]/10 text-[var(--color-primary)] text-xs rounded-full border border-[var(--color-primary)]/20">
{column}
</span>
))}
</div>
</Card>
)}
{/* Backend Recommendations */}
{results.recommendations?.length > 0 && (
<Card className="p-4">
<h4 className="font-semibold text-[var(--text-primary)] mb-3">Recomendaciones</h4>
<ul className="space-y-2">
{results.recommendations.slice(0, 3).map((rec, index) => (
<li key={index} className="text-sm text-[var(--text-secondary)] flex items-start">
<span className="text-[var(--color-success)] mr-2"></span>
{rec}
</li>
))}
</ul>
</Card>
)}
</div>
)}
</div> </div>
)} )}

View File

@@ -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 { Package, Calendar, AlertTriangle, Plus, Edit, Trash2, CheckCircle } from 'lucide-react';
import { Button, Card, Input, Badge } from '../../../ui'; import { Button, Card, Input, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard'; import { OnboardingStepProps } from '../OnboardingWizard';
@@ -31,8 +31,8 @@ interface InventoryItem {
const convertProductsToInventory = (approvedProducts: any[]): InventoryItem[] => { const convertProductsToInventory = (approvedProducts: any[]): InventoryItem[] => {
return approvedProducts.map((product, index) => ({ return approvedProducts.map((product, index) => ({
id: `inventory-${index}`, id: `inventory-${index}`,
name: product.suggested_name || product.name, name: product.suggested_name || product.original_name,
category: product.product_type || 'finished_product', category: product.product_type === 'ingredient' ? 'ingredient' : 'finished_product',
current_stock: 0, // To be configured by user current_stock: 0, // To be configured by user
min_stock: 1, // Default minimum min_stock: 1, // Default minimum
max_stock: 100, // Default maximum max_stock: 100, // Default maximum
@@ -62,8 +62,9 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
const currentTenant = useCurrentTenant(); const currentTenant = useCurrentTenant();
const { showToast } = useToast(); const { showToast } = useToast();
// Use the onboarding hooks // Use the business onboarding hooks
const { const {
updateStepData,
createInventoryFromSuggestions, createInventoryFromSuggestions,
importSalesData, importSalesData,
inventorySetup: { inventorySetup: {
@@ -72,6 +73,7 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
salesImportResult, salesImportResult,
isInventoryConfigured isInventoryConfigured
}, },
allStepData,
isLoading, isLoading,
error, error,
clearError clearError
@@ -79,11 +81,16 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
const createAlert = (alert: any) => { const createAlert = (alert: any) => {
console.log('Alert:', alert); console.log('Alert:', alert);
showToast({ if (showToast && typeof showToast === 'function') {
title: alert.title, showToast({
message: alert.message, title: alert.title,
type: alert.type 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 // Use modal for confirmations and editing
@@ -93,6 +100,7 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [editingItem, setEditingItem] = useState<InventoryItem | null>(null); const [editingItem, setEditingItem] = useState<InventoryItem | null>(null);
const [isAddingNew, setIsAddingNew] = useState(false); const [isAddingNew, setIsAddingNew] = useState(false);
const [inventoryCreationAttempted, setInventoryCreationAttempted] = useState(false);
// Generate inventory items from approved products // Generate inventory items from approved products
const generateInventoryFromProducts = (approvedProducts: any[]): InventoryItem[] => { const generateInventoryFromProducts = (approvedProducts: any[]): InventoryItem[] => {
@@ -115,20 +123,24 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
if (data.inventoryItems) { if (data.inventoryItems) {
return data.inventoryItems; return data.inventoryItems;
} }
// Try to get approved products from current step data first, then from review step data // Try to get approved products from business hooks data first, then from component props
const approvedProducts = data.approvedProducts || data.allStepData?.['review']?.approvedProducts; const approvedProducts = data.approvedProducts ||
allStepData?.['review']?.approvedProducts ||
data.allStepData?.['review']?.approvedProducts;
return generateInventoryFromProducts(approvedProducts || []); return generateInventoryFromProducts(approvedProducts || []);
}); });
// Update items when approved products become available (for when component is already mounted) // Update items when approved products become available (for when component is already mounted)
useEffect(() => { 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) { if (approvedProducts && approvedProducts.length > 0 && items.length === 0) {
const newItems = generateInventoryFromProducts(approvedProducts); const newItems = generateInventoryFromProducts(approvedProducts);
setItems(newItems); setItems(newItems);
} }
}, [data.approvedProducts, data.allStepData]); }, [data.approvedProducts, allStepData, data.allStepData]);
const [filterCategory, setFilterCategory] = useState<'all' | 'ingredient' | 'finished_product'>('all'); const [filterCategory, setFilterCategory] = useState<'all' | 'ingredient' | 'finished_product'>('all');
@@ -136,20 +148,25 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
? items ? items
: items.filter(item => item.category === filterCategory); : items.filter(item => item.category === filterCategory);
// Create inventory items via API using hooks // Create inventory items via API using business hooks
const handleCreateInventory = async () => { const handleCreateInventory = async () => {
console.log('InventorySetup - Starting handleCreateInventory'); 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; const approvedProducts = data.approvedProducts ||
console.log('InventorySetup - approvedProducts:', 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 // Get tenant ID from current tenant context or user
const tenantId = currentTenant?.id || user?.tenant_id; const tenantId = currentTenant?.id || user?.tenant_id;
console.log('InventorySetup - tenantId from currentTenant:', currentTenant?.id); console.log('InventorySetup - tenantId:', tenantId);
console.log('InventorySetup - tenantId from user:', user?.tenant_id);
console.log('InventorySetup - final tenantId:', tenantId);
if (!tenantId || !approvedProducts || approvedProducts.length === 0) { if (!tenantId || !approvedProducts || approvedProducts.length === 0) {
console.log('InventorySetup - Missing requirements: tenantId =', tenantId, 'approvedProducts length =', approvedProducts?.length); console.log('InventorySetup - Missing requirements: tenantId =', tenantId, 'approvedProducts length =', approvedProducts?.length);
@@ -167,162 +184,112 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
setIsCreating(true); setIsCreating(true);
try { try {
// Create ingredients one by one using the inventory hook // Approved products should already be in ProductSuggestionResponse format
let successCount = 0; // Just ensure they have all required fields
let failCount = 0; const suggestions = approvedProducts.map((product: any, index: number) => ({
const createdItems: any[] = []; suggestion_id: product.suggestion_id || `suggestion-${Date.now()}-${index}`,
const inventoryMapping: { [productName: string]: string } = {}; original_name: product.original_name,
suggested_name: product.suggested_name,
for (const [index, product] of approvedProducts.entries()) { product_type: product.product_type,
const ingredientData = { category: product.category,
name: product.suggested_name || product.name, unit_of_measure: product.unit_of_measure,
category: product.category || 'general', confidence_score: product.confidence_score || 0.8,
unit_of_measure: product.unit_of_measure || 'unit', estimated_shelf_life_days: product.estimated_shelf_life_days,
shelf_life_days: product.estimated_shelf_life_days || 30, requires_refrigeration: product.requires_refrigeration,
requires_refrigeration: product.requires_refrigeration || false, requires_freezing: product.requires_freezing,
requires_freezing: product.requires_freezing || false, is_seasonal: product.is_seasonal,
is_seasonal: product.is_seasonal || false, suggested_supplier: product.suggested_supplier,
minimum_stock_level: 0, notes: product.notes
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;
}
}
// Show results // Use business onboarding hook to create inventory
if (successCount > 0) { const inventorySuccess = await createInventoryFromSuggestions(suggestions);
if (inventorySuccess) {
createAlert({ createAlert({
type: 'success', type: 'success',
category: 'system', category: 'system',
priority: 'medium', priority: 'medium',
title: 'Inventario creado', title: 'Inventario creado',
message: `Se crearon ${successCount} elementos de inventario exitosamente.`, message: `Se crearon ${suggestions.length} elementos de inventario exitosamente.`,
source: 'onboarding' 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({ createAlert({
type: 'error', type: 'error',
category: 'system', category: 'system',
priority: 'high', priority: 'high',
title: 'Error al crear inventario', 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' 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) { } catch (error) {
console.error('Error creating inventory:', error); console.error('Error creating inventory:', error);
const errorMessage = error instanceof Error ? error.message : 'Error al crear inventario'; const errorMessage = error instanceof Error ? error.message : 'Error al crear inventario';
@@ -339,17 +306,34 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
} }
}; };
const lastItemsRef = useRef(items);
const lastIsCreatingRef = useRef(isCreating);
useEffect(() => { useEffect(() => {
const hasValidStock = items.length > 0 && items.every(item => // Only update if items or isCreating actually changed
item.min_stock >= 0 && item.max_stock > item.min_stock if (JSON.stringify(items) !== JSON.stringify(lastItemsRef.current) || isCreating !== lastIsCreatingRef.current) {
); lastItemsRef.current = items;
lastIsCreatingRef.current = isCreating;
onDataChange({
...data, const hasValidStock = items.length > 0 && items.every(item =>
inventoryItems: items, item.min_stock >= 0 && item.max_stock > item.min_stock
inventoryConfigured: hasValidStock && !isCreating );
});
}, [items, isCreating]); 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) // Auto-create inventory when step is completed (when user clicks Next)
useEffect(() => { useEffect(() => {
@@ -358,11 +342,12 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
); );
// If inventory is configured but not yet created in backend, create it automatically // 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'); console.log('InventorySetup - Auto-creating inventory on step completion');
setInventoryCreationAttempted(true);
handleCreateInventory(); handleCreateInventory();
} }
}, [data.inventoryCreated, items, isCreating]); }, [data.inventoryCreated, items, isCreating, inventoryCreationAttempted]);
const handleAddItem = () => { const handleAddItem = () => {
const newItem: InventoryItem = { const newItem: InventoryItem = {

View File

@@ -168,14 +168,13 @@ export const ReviewStep: React.FC<OnboardingStepProps> = ({
approvedProductsCount: approvedProducts.length approvedProductsCount: approvedProducts.length
}); });
onDataChange({ onDataChange({
...data,
detectedProducts: products, detectedProducts: products,
approvedProducts, approvedProducts,
reviewCompleted reviewCompleted
}); });
dataChangeRef.current = currentState; dataChangeRef.current = currentState;
} }
}, [products, approvedProducts, reviewCompleted]); }, [products, approvedProducts, reviewCompleted, onDataChange]);
// Handle review completion alert separately // Handle review completion alert separately
useEffect(() => { useEffect(() => {

View File

@@ -65,7 +65,14 @@ export const useInventorySetup = () => {
createdItems?: any[]; createdItems?: any[];
inventoryMapping?: { [productName: string]: string }; 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) { if (!suggestions || suggestions.length === 0) {
console.error('useInventorySetup - No suggestions provided');
setState(prev => ({ setState(prev => ({
...prev, ...prev,
error: 'No hay sugerencias para crear el inventario', error: 'No hay sugerencias para crear el inventario',
@@ -74,6 +81,7 @@ export const useInventorySetup = () => {
} }
if (!currentTenant?.id) { if (!currentTenant?.id) {
console.error('useInventorySetup - No tenant ID available');
setState(prev => ({ setState(prev => ({
...prev, ...prev,
error: 'No se pudo obtener información del tenant', error: 'No se pudo obtener información del tenant',
@@ -91,31 +99,54 @@ export const useInventorySetup = () => {
const createdItems = []; const createdItems = [];
const inventoryMapping: { [key: string]: string } = {}; const inventoryMapping: { [key: string]: string } = {};
console.log('useInventorySetup - Creating ingredients from suggestions...');
// Create ingredients from approved suggestions // Create ingredients from approved suggestions
for (const suggestion of suggestions) { for (const suggestion of suggestions) {
try { try {
const ingredientData = { const ingredientData = {
name: suggestion.name, name: suggestion.suggested_name || suggestion.original_name,
category: suggestion.category || 'Sin categoría', category: suggestion.category || 'Sin categoría',
description: suggestion.description || '', description: suggestion.notes || '',
unit_of_measure: suggestion.unit_of_measure || 'unidad', unit_of_measure: suggestion.unit_of_measure || 'unidad',
cost_per_unit: suggestion.cost_per_unit || 0, minimum_stock_level: 1, // Default minimum stock
supplier_info: suggestion.supplier_info || {}, maximum_stock_level: 100, // Default maximum stock
nutritional_info: suggestion.nutritional_info || {}, reorder_point: 5, // Default reorder point
storage_requirements: suggestion.storage_requirements || {}, shelf_life_days: suggestion.estimated_shelf_life_days || 30,
allergen_info: suggestion.allergen_info || {}, requires_refrigeration: suggestion.requires_refrigeration || false,
is_active: true, 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({ const createdItem = await createIngredientMutation.mutateAsync({
tenantId: currentTenant.id, tenantId: currentTenant.id,
ingredientData, ingredientData,
}); });
console.log('useInventorySetup - Created ingredient successfully:', {
id: createdItem.id,
name: createdItem.name
});
createdItems.push(createdItem); 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) { } 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 // Continue with other ingredients even if one fails
} }
} }
@@ -123,6 +154,12 @@ export const useInventorySetup = () => {
if (createdItems.length === 0) { if (createdItems.length === 0) {
throw new Error('No se pudo crear ningún elemento del inventario'); 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 => ({ setState(prev => ({
...prev, ...prev,
@@ -139,6 +176,7 @@ export const useInventorySetup = () => {
}; };
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error creando el inventario'; const errorMessage = error instanceof Error ? error.message : 'Error creando el inventario';
console.error('useInventorySetup - Error in createInventoryFromSuggestions:', error);
setState(prev => ({ setState(prev => ({
...prev, ...prev,
isLoading: false, isLoading: false,

View File

@@ -20,35 +20,6 @@ import type {
} from './types'; } from './types';
import type { BakeryRegistration } from '../../../api'; import type { BakeryRegistration } from '../../../api';
interface OnboardingActions {
// Navigation
nextStep: () => boolean;
previousStep: () => boolean;
goToStep: (stepIndex: number) => boolean;
// Data Management
updateStepData: (stepId: string, data: Partial<OnboardingData>) => void;
validateCurrentStep: () => string | null;
// Step-specific Actions
createTenant: (bakeryData: BakeryRegistration) => Promise<boolean>;
processSalesFile: (file: File, onProgress?: ProgressCallback) => Promise<boolean>;
generateInventorySuggestions: (productList: string[]) => Promise<ProductSuggestionResponse[] | null>;
createInventoryFromSuggestions: (suggestions: ProductSuggestionResponse[]) => Promise<boolean>;
importSalesData: (salesData: any, inventoryMapping: { [productName: string]: string }) => Promise<boolean>;
startTraining: (options?: {
products?: string[];
startDate?: string;
endDate?: string;
}) => Promise<boolean>;
// Completion
completeOnboarding: () => Promise<boolean>;
// Utilities
clearError: () => void;
reset: () => void;
}
export const useOnboarding = () => { export const useOnboarding = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -63,28 +34,6 @@ export const useOnboarding = () => {
const inventorySetup = useInventorySetup(); const inventorySetup = useInventorySetup();
const trainingOrchestration = useTrainingOrchestration(); 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 // Data management
const updateStepData = useCallback((stepId: string, stepData: Partial<OnboardingData>) => { const updateStepData = useCallback((stepId: string, stepData: Partial<OnboardingData>) => {
data.updateStepData(stepId, stepData); data.updateStepData(stepId, stepData);
@@ -104,10 +53,69 @@ export const useOnboarding = () => {
const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => { const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => {
const success = await tenantCreation.createTenant(bakeryData); const success = await tenantCreation.createTenant(bakeryData);
if (success) { 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; return success;
}, [tenantCreation, updateStepData]); }, [tenantCreation, updateStepData, currentTenant]);
// Navigation actions
const nextStep = useCallback(async (): Promise<boolean> => {
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 ( const processSalesFile = useCallback(async (
file: File, file: File,
@@ -299,8 +307,5 @@ export const useOnboarding = () => {
completeOnboarding, completeOnboarding,
clearError, clearError,
reset, reset,
} satisfies ReturnType<typeof useOnboardingFlow> & };
ReturnType<typeof useOnboardingData> &
{ [key: string]: any } &
OnboardingActions;
}; };

View File

@@ -3,7 +3,7 @@
*/ */
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { useClassifyProductsBatch, useValidateAndImportFile } from '../../../api'; import { useClassifyProductsBatch, useValidateFileOnly } from '../../../api';
import { useCurrentTenant } from '../../../stores'; import { useCurrentTenant } from '../../../stores';
import type { ProductSuggestionResponse, ProgressCallback } from './types'; import type { ProductSuggestionResponse, ProgressCallback } from './types';
@@ -41,7 +41,7 @@ export const useSalesProcessing = () => {
const classifyProductsMutation = useClassifyProductsBatch(); const classifyProductsMutation = useClassifyProductsBatch();
const currentTenant = useCurrentTenant(); const currentTenant = useCurrentTenant();
const { processFile: processFileImport } = useValidateAndImportFile(); const { validateFile } = useValidateFileOnly();
const updateProgress = useCallback((progress: number, stage: string, message: string, onProgress?: ProgressCallback) => { const updateProgress = useCallback((progress: number, stage: string, message: string, onProgress?: ProgressCallback) => {
setState(prev => ({ setState(prev => ({
@@ -76,21 +76,35 @@ export const useSalesProcessing = () => {
updateProgress(20, 'validating', 'Validando estructura del archivo...', onProgress); updateProgress(20, 'validating', 'Validando estructura del archivo...', onProgress);
// Use the actual data import hook for file validation // 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) { 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, currentTenant.id,
file, file,
{ {
skipValidation: false,
onProgress: (stage, progress, message) => { onProgress: (stage, progress, message) => {
updateProgress(progress, stage, message, onProgress); 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) { if (!result.success || !result.validationResult) {
throw new Error(result.error || 'Error en la validación del archivo'); throw new Error(result.error || 'Error en la validación del archivo');
} }
@@ -100,9 +114,21 @@ export const useSalesProcessing = () => {
product_list: extractProductList(result.validationResult), 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); updateProgress(40, 'validating', 'Verificando integridad de datos...', onProgress);
if (!validationResult || !validationResult.product_list || validationResult.product_list.length === 0) { 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'); throw new Error('No se encontraron productos válidos en el archivo');
} }
@@ -144,11 +170,24 @@ export const useSalesProcessing = () => {
success: false, success: false,
}; };
} }
}, [updateProgress, currentTenant, processFileImport]); }, [updateProgress, currentTenant, validateFile]);
// Helper to extract product list from validation result // Helper to extract product list from validation result
const extractProductList = useCallback((validationResult: any): string[] => { 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)) { if (validationResult.sample_records && Array.isArray(validationResult.sample_records)) {
const productSet = new Set<string>(); const productSet = new Set<string>();
validationResult.sample_records.forEach((record: any) => { validationResult.sample_records.forEach((record: any) => {
@@ -156,8 +195,12 @@ export const useSalesProcessing = () => {
productSet.add(record.product_name); 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 []; return [];
}, []); }, []);

View File

@@ -4,7 +4,8 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { useRegisterBakery } from '../../../api'; import { useRegisterBakery } from '../../../api';
import type { BakeryRegistration } from '../../../api'; import type { BakeryRegistration, TenantResponse } from '../../../api';
import { useTenantStore } from '../../../stores/tenant.store';
interface TenantCreationState { interface TenantCreationState {
isLoading: boolean; isLoading: boolean;
@@ -28,6 +29,7 @@ export const useTenantCreation = () => {
}); });
const registerBakeryMutation = useRegisterBakery(); const registerBakeryMutation = useRegisterBakery();
const { setCurrentTenant, loadUserTenants } = useTenantStore();
const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => { const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => {
if (!bakeryData) { if (!bakeryData) {
@@ -46,7 +48,14 @@ export const useTenantCreation = () => {
})); }));
try { 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 => ({ setState(prev => ({
...prev, ...prev,
@@ -55,6 +64,7 @@ export const useTenantCreation = () => {
tenantData: bakeryData, tenantData: bakeryData,
})); }));
console.log('useTenantCreation - Tenant created and set successfully');
return true; return true;
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error al crear la panadería'; const errorMessage = error instanceof Error ? error.message : 'Error al crear la panadería';

View File

@@ -68,19 +68,25 @@ const OnboardingPage: React.FC = () => {
} }
}; };
const handleNext = () => { const handleNext = async (): Promise<boolean> => {
return nextStep(); try {
const success = await nextStep();
return success;
} catch (error) {
console.error('Error in handleNext:', error);
return false;
}
}; };
const handlePrevious = () => { const handlePrevious = (): boolean => {
return previousStep(); return previousStep();
}; };
const handleComplete = async (allData: any) => { const handleComplete = async (allData: any): Promise<void> => {
const success = await completeOnboarding(); try {
if (success) { await completeOnboarding();
// Navigation is handled inside completeOnboarding } catch (error) {
return; console.error('Error in handleComplete:', error);
} }
}; };

View File

@@ -40,7 +40,7 @@ export interface AuthState {
canAccess: (resource: string, action: string) => boolean; canAccess: (resource: string, action: string) => boolean;
} }
import { authService } from '../api'; import { authService, apiClient } from '../api';
export const useAuthStore = create<AuthState>()( export const useAuthStore = create<AuthState>()(
persist( persist(
@@ -61,6 +61,9 @@ export const useAuthStore = create<AuthState>()(
const response = await authService.login({ email, password }); const response = await authService.login({ email, password });
if (response && response.access_token) { if (response && response.access_token) {
// Set the auth token on the API client immediately
apiClient.setAuthToken(response.access_token);
set({ set({
user: response.user || null, user: response.user || null,
token: response.access_token, token: response.access_token,
@@ -92,6 +95,9 @@ export const useAuthStore = create<AuthState>()(
const response = await authService.register(userData); const response = await authService.register(userData);
if (response && response.access_token) { if (response && response.access_token) {
// Set the auth token on the API client immediately
apiClient.setAuthToken(response.access_token);
set({ set({
user: response.user || null, user: response.user || null,
token: response.access_token, token: response.access_token,
@@ -117,6 +123,10 @@ export const useAuthStore = create<AuthState>()(
}, },
logout: () => { logout: () => {
// Clear the auth token from API client
apiClient.setAuthToken(null);
apiClient.setTenantId(null);
set({ set({
user: null, user: null,
token: null, token: null,
@@ -139,6 +149,9 @@ export const useAuthStore = create<AuthState>()(
const response = await authService.refreshToken(refreshToken); const response = await authService.refreshToken(refreshToken);
if (response && response.access_token) { if (response && response.access_token) {
// Set the auth token on the API client immediately
apiClient.setAuthToken(response.access_token);
set({ set({
token: response.access_token, token: response.access_token,
refreshToken: response.refresh_token || refreshToken, refreshToken: response.refresh_token || refreshToken,