From 94b39ebdfb6399591a6b09af183412f2193136a7 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Wed, 23 Jul 2025 15:41:41 +0200 Subject: [PATCH] Add new frontend - fix 22 --- frontend/src/api/services/dataService.ts | 210 +++++++++++++++-------- frontend/src/pages/onboarding.tsx | 155 +++++++++++++---- 2 files changed, 257 insertions(+), 108 deletions(-) diff --git a/frontend/src/api/services/dataService.ts b/frontend/src/api/services/dataService.ts index 9ee68f27..8178b34e 100644 --- a/frontend/src/api/services/dataService.ts +++ b/frontend/src/api/services/dataService.ts @@ -1,8 +1,6 @@ -// src/api/services/DataService.ts +// frontend/src/api/services/dataService.ts - COMPLETE FIX import { apiClient } from '../base/apiClient'; -import { - ApiResponse -} from '../types/api'; +import { ApiResponse } from '../types/api'; export interface DashboardStats { totalSales: number; @@ -21,12 +19,28 @@ export interface UploadResponse { upload_id?: string; } +// FIXED: Updated to match backend SalesValidationResult schema export interface DataValidation { - valid: boolean; - errors: string[]; - warnings: string[]; - recordCount: number; - duplicates: number; + is_valid: boolean; // Changed from 'valid' to 'is_valid' + total_records: number; // Changed from 'recordCount' to 'total_records' + valid_records: number; // Added missing field + invalid_records: number; // Added missing field + errors: Array<{ // Changed from string[] to object array + row?: number; + field?: string; + message: string; + value?: any; + }>; + warnings: Array<{ // Changed from string[] to object array + row?: number; + field?: string; + message: string; + value?: any; + }>; + summary: { // Added missing summary field + [key: string]: any; + }; + duplicates?: number; // Made optional, may not always be present } // Data types @@ -61,6 +75,15 @@ export interface CreateSalesRequest { date: string; } +// FIXED: Interface for import data that matches backend SalesDataImport schema +export interface SalesDataImportRequest { + tenant_id: string; + data: string; // File content as string + data_format: 'csv' | 'json' | 'excel'; + source?: string; + validate_only?: boolean; +} + export class DataService { /** * Upload sales history file @@ -78,14 +101,80 @@ export class DataService { } /** - * Validate sales data before upload + * FIXED: Validate sales data before upload + * Backend expects JSON data with SalesDataImport structure, not a file upload */ async validateSalesData(file: File): Promise { - const response = await apiClient.upload>( - '/api/v1/sales/import/validate-sales', - file - ); - return response.data!; + try { + // Read file content + const fileContent = await this.readFileAsText(file); + + // Determine file format from extension + const fileName = file.name.toLowerCase(); + let dataFormat: 'csv' | 'json' | 'excel'; + + if (fileName.endsWith('.csv')) { + dataFormat = 'csv'; + } else if (fileName.endsWith('.json')) { + dataFormat = 'json'; + } else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) { + dataFormat = 'excel'; + } else { + // Default to CSV if unable to determine + dataFormat = 'csv'; + } + + // FIXED: Use the correct endpoint and send JSON data instead of file upload + const importData: SalesDataImportRequest = { + tenant_id: '', // Will be set by backend from auth context + data: fileContent, + data_format: dataFormat, + validate_only: true + }; + + const response = await apiClient.post>( + '/api/v1/data/sales/import/validate', // Fixed endpoint path + importData + ); + + return response.data!; + } catch (error: any) { + // Handle validation errors gracefully + if (error.response?.status === 422) { + // Return a failed validation result instead of throwing + return { + is_valid: false, + total_records: 0, + valid_records: 0, + invalid_records: 0, + errors: [{ + message: error.response?.data?.detail || 'Validation failed' + }], + warnings: [], + summary: { + validation_error: true, + message: error.response?.data?.detail || 'File validation failed' + } + }; + } + throw error; + } + } + + /** + * Helper method to read file as text + */ + private readFileAsText(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (event) => { + resolve(event.target?.result as string); + }; + reader.onerror = () => { + reject(new Error('Failed to read file')); + }; + reader.readAsText(file); + }); } /** @@ -129,66 +218,42 @@ export class DataService { } /** - * Update sales record + * FIXED: Import sales data (actual import after validation) */ - async updateSalesRecord( - id: string, - updates: Partial - ): Promise { - const response = await apiClient.put>( - `/api/v1/data/sales/${id}`, - updates - ); - return response.data!; - } + async importSalesData(file: File): Promise { + try { + const fileContent = await this.readFileAsText(file); + + // Determine file format + const fileName = file.name.toLowerCase(); + let dataFormat: 'csv' | 'json' | 'excel'; + + if (fileName.endsWith('.csv')) { + dataFormat = 'csv'; + } else if (fileName.endsWith('.json')) { + dataFormat = 'json'; + } else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) { + dataFormat = 'excel'; + } else { + dataFormat = 'csv'; + } - /** - * Delete sales record - */ - async deleteSalesRecord(id: string): Promise { - await apiClient.delete(`/api/v1/data/sales/${id}`); - } + const importData: SalesDataImportRequest = { + tenant_id: '', // Will be set by backend from auth context + data: fileContent, + data_format: dataFormat, + validate_only: false + }; - /** - * Get weather data - */ - async getWeatherData(params?: { - startDate?: string; - endDate?: string; - location?: string; - }): Promise { - const response = await apiClient.get>( - '/api/v1/data/weather', - { params } - ); - return response.data!; - } - - /** - * Get traffic data - */ - async getTrafficData(params?: { - startDate?: string; - endDate?: string; - location?: string; - }): Promise { - const response = await apiClient.get>( - '/api/v1/data/traffic', - { params } - ); - return response.data!; - } - - /** - * Get data quality report - */ - async getDataQuality(): Promise<{ - salesData: { completeness: number; quality: number; lastUpdate: string }; - weatherData: { completeness: number; quality: number; lastUpdate: string }; - trafficData: { completeness: number; quality: number; lastUpdate: string }; - }> { - const response = await apiClient.get>('/api/v1/data/quality'); - return response.data!; + const response = await apiClient.post>( + '/api/v1/data/sales/import', + importData + ); + + return response.data!; + } catch (error: any) { + throw error; + } } /** @@ -233,4 +298,5 @@ export class DataService { } } +// CRITICAL: Export the instance with the name expected by the index file export const dataService = new DataService(); \ No newline at end of file diff --git a/frontend/src/pages/onboarding.tsx b/frontend/src/pages/onboarding.tsx index 02d7a804..c2298e4d 100644 --- a/frontend/src/pages/onboarding.tsx +++ b/frontend/src/pages/onboarding.tsx @@ -17,9 +17,11 @@ import { import { TenantUser, TenantCreate, - TenantInfo + TenantInfo , + DataValidation } from '@/api/services'; + import api from '@/api/services'; import { useAuth } from '../contexts/AuthContext'; @@ -206,27 +208,86 @@ const OnboardingPage = () => { setCurrentTenantId(tenant.id); showNotification('success', 'Panadería registrada', 'Información guardada correctamente.'); - } else if (currentStep === 3) { - // Upload and validate sales data using real data service - if (formData.salesFile) { - // First validate the data - const validation = await api.data.validateSalesData(formData.salesFile); - setUploadValidation(validation); - - if (validation.valid || validation.warnings.length === 0) { - // Upload the file - const uploadResult = await api.data.uploadSalesHistory( - formData.salesFile, - { tenant_id: currentTenantId } - ); - showNotification('success', 'Archivo subido', `${uploadResult.records_processed} registros procesados.`); - } else { - showNotification('warning', 'Datos con advertencias', 'Se encontraron algunas advertencias pero los datos son válidos.'); + } else if (currentStep === 3) { + // FIXED: Sales upload step with proper validation handling + if (formData.salesFile) { + try { + // Validate if not already validated + let validation = uploadValidation; + if (!validation) { + validation = await api.data.validateSalesData(formData.salesFile); + setUploadValidation(validation); + } + + // FIXED: Check validation using correct field names + if (!validation.is_valid) { + const errorMessages = validation.errors.map(error => + `${error.row ? `Fila ${error.row}: ` : ''}${error.message}` + ).join('; '); + + showNotification('error', 'Datos inválidos', + `Se encontraron errores: ${errorMessages.slice(0, 200)}${errorMessages.length > 200 ? '...' : ''}`); + setLoading(false); + return; + } + + // Show warnings if any + if (validation.warnings.length > 0) { + const warningMessages = validation.warnings.map(warning => + `${warning.row ? `Fila ${warning.row}: ` : ''}${warning.message}` + ).join('; '); + + showNotification('warning', 'Advertencias encontradas', + `Advertencias: ${warningMessages.slice(0, 200)}${warningMessages.length > 200 ? '...' : ''}`); + } + + // Proceed with import - Use the existing uploadSalesHistory method + // or create a new importSalesData method + const uploadResult = await api.data.uploadSalesHistory( + formData.salesFile, + { tenant_id: currentTenantId } + ); + + showNotification('success', 'Archivo subido', + `${uploadResult.records_processed} registros procesados exitosamente.`); + + console.log('Upload successful:', { + records_processed: uploadResult.records_processed, + validation_summary: { + total_records: validation.total_records, + valid_records: validation.valid_records, + invalid_records: validation.invalid_records, + errors_count: validation.errors.length, + warnings_count: validation.warnings.length + } + }); + + } catch (error: any) { + console.error('Sales upload error:', error); + + let errorMessage = 'No se pudo procesar el archivo de ventas.'; + let errorTitle = 'Error al subir'; + + if (error.response?.status === 422) { + errorTitle = 'Error de validación'; + errorMessage = error.response.data?.detail || 'Formato de datos incorrecto'; + } else if (error.response?.status === 400) { + errorTitle = 'Archivo inválido'; + errorMessage = error.response.data?.detail || 'El formato del archivo no es compatible'; + } else if (error.response?.status >= 500) { + errorTitle = 'Error del servidor'; + errorMessage = 'Problema temporal del servidor. Inténtalo más tarde.'; + } else if (error.message) { + errorMessage = error.message; + } + + showNotification('error', errorTitle, errorMessage); + setLoading(false); + return; + } } - } - } else if (currentStep === 4) { - // Start training using real training service + // Training step const trainingConfig = { include_weather: true, include_traffic: true, @@ -245,7 +306,6 @@ const OnboardingPage = () => { })); showNotification('info', 'Entrenamiento iniciado', 'Los modelos se están entrenando...'); - // Don't move to next step automatically, wait for training completion setLoading(false); return; } @@ -289,7 +349,7 @@ const OnboardingPage = () => { } }; - const handleFileUpload = async (event) => { + const handleFileUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { setFormData(prev => ({ ...prev, salesFile: file })); @@ -298,33 +358,56 @@ const OnboardingPage = () => { // Auto-validate file on selection try { setLoading(true); + console.log('Validating file:', file.name); + + // FIXED: Use the corrected validation method const validation = await api.data.validateSalesData(file); setUploadValidation(validation); - if (validation.valid) { - showNotification('success', 'Archivo válido', `${validation.recordCount} registros detectados.`); + // FIXED: Use correct field names from backend response + if (validation.is_valid) { + showNotification('success', 'Archivo válido', + `${validation.total_records} registros detectados, ${validation.valid_records} válidos.`); } else if (validation.warnings.length > 0 && validation.errors.length === 0) { - showNotification('warning', 'Archivo con advertencias', 'El archivo es válido pero tiene algunas advertencias.'); + showNotification('warning', 'Archivo con advertencias', + 'El archivo es válido pero tiene algunas advertencias.'); } else { - showNotification('error', 'Archivo inválido', 'El archivo tiene errores que deben corregirse.'); + const errorCount = validation.errors.length; + showNotification('error', 'Archivo con errores', + `Se encontraron ${errorCount} errores en el archivo.`); } - } catch (error) { + + } catch (error: any) { console.error('Error validating file:', error); - showNotification('error', 'Error de validación', 'No se pudo validar el archivo.'); + + // Handle network or other errors + let errorMessage = 'Error al validar el archivo'; + if (error.response?.status === 422) { + errorMessage = 'Formato de archivo inválido'; + } else if (error.response?.status === 400) { + errorMessage = 'El archivo no se puede procesar'; + } else if (error.message) { + errorMessage = error.message; + } + + showNotification('error', 'Error de validación', errorMessage); + + // Set a failed validation state + setUploadValidation({ + is_valid: false, + total_records: 0, + valid_records: 0, + invalid_records: 0, + errors: [{ message: errorMessage }], + warnings: [], + summary: { validation_failed: true } + }); } finally { setLoading(false); } } }; - const handleFinalSubmit = () => { - showNotification('success', '¡Configuración completa!', 'Tu sistema está listo para usar.'); - setTimeout(() => { - // Navigate to dashboard - window.location.href = '/dashboard'; - }, 2000); - }; - const renderStepIndicator = () => (