Start integrating the onboarding flow with backend 10
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
@@ -384,6 +384,7 @@ export {
|
|||||||
useValidateCsvFile,
|
useValidateCsvFile,
|
||||||
useImportJsonData,
|
useImportJsonData,
|
||||||
useImportCsvFile,
|
useImportCsvFile,
|
||||||
|
useValidateFileOnly,
|
||||||
useValidateAndImportFile,
|
useValidateAndImportFile,
|
||||||
dataImportKeys,
|
dataImportKeys,
|
||||||
} from './hooks/dataImport';
|
} from './hooks/dataImport';
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 || {}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
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
|
||||||
|
}));
|
||||||
|
|
||||||
for (const [index, product] of approvedProducts.entries()) {
|
// Use business onboarding hook to create inventory
|
||||||
const ingredientData = {
|
const inventorySuccess = await createInventoryFromSuggestions(suggestions);
|
||||||
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 {
|
if (inventorySuccess) {
|
||||||
// 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
|
|
||||||
if (successCount > 0) {
|
|
||||||
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> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const lastItemsRef = useRef(items);
|
||||||
const hasValidStock = items.length > 0 && items.every(item =>
|
const lastIsCreatingRef = useRef(isCreating);
|
||||||
item.min_stock >= 0 && item.max_stock > item.min_stock
|
|
||||||
);
|
|
||||||
|
|
||||||
onDataChange({
|
useEffect(() => {
|
||||||
...data,
|
// Only update if items or isCreating actually changed
|
||||||
inventoryItems: items,
|
if (JSON.stringify(items) !== JSON.stringify(lastItemsRef.current) || isCreating !== lastIsCreatingRef.current) {
|
||||||
inventoryConfigured: hasValidStock && !isCreating
|
lastItemsRef.current = items;
|
||||||
});
|
lastIsCreatingRef.current = isCreating;
|
||||||
}, [items, 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)
|
// 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 = {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +155,12 @@ export const useInventorySetup = () => {
|
|||||||
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,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
};
|
||||||
@@ -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 [];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user