Add new frontend - fix 22

This commit is contained in:
Urtzi Alfaro
2025-07-23 15:41:41 +02:00
parent 96b728441a
commit 94b39ebdfb
2 changed files with 257 additions and 108 deletions

View File

@@ -1,8 +1,6 @@
// src/api/services/DataService.ts // frontend/src/api/services/dataService.ts - COMPLETE FIX
import { apiClient } from '../base/apiClient'; import { apiClient } from '../base/apiClient';
import { import { ApiResponse } from '../types/api';
ApiResponse
} from '../types/api';
export interface DashboardStats { export interface DashboardStats {
totalSales: number; totalSales: number;
@@ -21,12 +19,28 @@ export interface UploadResponse {
upload_id?: string; upload_id?: string;
} }
// FIXED: Updated to match backend SalesValidationResult schema
export interface DataValidation { export interface DataValidation {
valid: boolean; is_valid: boolean; // Changed from 'valid' to 'is_valid'
errors: string[]; total_records: number; // Changed from 'recordCount' to 'total_records'
warnings: string[]; valid_records: number; // Added missing field
recordCount: number; invalid_records: number; // Added missing field
duplicates: number; 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 // Data types
@@ -61,6 +75,15 @@ export interface CreateSalesRequest {
date: string; 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 { export class DataService {
/** /**
* Upload sales history file * 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<DataValidation> { async validateSalesData(file: File): Promise<DataValidation> {
const response = await apiClient.upload<ApiResponse<DataValidation>>( try {
'/api/v1/sales/import/validate-sales', // Read file content
file const fileContent = await this.readFileAsText(file);
);
return response.data!; // 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<ApiResponse<DataValidation>>(
'/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<string> {
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( async importSalesData(file: File): Promise<UploadResponse> {
id: string, try {
updates: Partial<CreateSalesRequest> const fileContent = await this.readFileAsText(file);
): Promise<SalesRecord> {
const response = await apiClient.put<ApiResponse<SalesRecord>>( // Determine file format
`/api/v1/data/sales/${id}`, const fileName = file.name.toLowerCase();
updates let dataFormat: 'csv' | 'json' | 'excel';
);
return response.data!; 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';
}
/** const importData: SalesDataImportRequest = {
* Delete sales record tenant_id: '', // Will be set by backend from auth context
*/ data: fileContent,
async deleteSalesRecord(id: string): Promise<void> { data_format: dataFormat,
await apiClient.delete(`/api/v1/data/sales/${id}`); validate_only: false
} };
/** const response = await apiClient.post<ApiResponse<UploadResponse>>(
* Get weather data '/api/v1/data/sales/import',
*/ importData
async getWeatherData(params?: { );
startDate?: string;
endDate?: string; return response.data!;
location?: string; } catch (error: any) {
}): Promise<WeatherData[]> { throw error;
const response = await apiClient.get<ApiResponse<WeatherData[]>>( }
'/api/v1/data/weather',
{ params }
);
return response.data!;
}
/**
* Get traffic data
*/
async getTrafficData(params?: {
startDate?: string;
endDate?: string;
location?: string;
}): Promise<TrafficData[]> {
const response = await apiClient.get<ApiResponse<TrafficData[]>>(
'/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<ApiResponse<any>>('/api/v1/data/quality');
return response.data!;
} }
/** /**
@@ -233,4 +298,5 @@ export class DataService {
} }
} }
// CRITICAL: Export the instance with the name expected by the index file
export const dataService = new DataService(); export const dataService = new DataService();

View File

@@ -17,9 +17,11 @@ import {
import { import {
TenantUser, TenantUser,
TenantCreate, TenantCreate,
TenantInfo TenantInfo ,
DataValidation
} from '@/api/services'; } from '@/api/services';
import api from '@/api/services'; import api from '@/api/services';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
@@ -206,27 +208,86 @@ const OnboardingPage = () => {
setCurrentTenantId(tenant.id); setCurrentTenantId(tenant.id);
showNotification('success', 'Panadería registrada', 'Información guardada correctamente.'); showNotification('success', 'Panadería registrada', 'Información guardada correctamente.');
} else if (currentStep === 3) { } else if (currentStep === 3) {
// Upload and validate sales data using real data service // FIXED: Sales upload step with proper validation handling
if (formData.salesFile) { if (formData.salesFile) {
// First validate the data try {
const validation = await api.data.validateSalesData(formData.salesFile); // Validate if not already validated
setUploadValidation(validation); let validation = uploadValidation;
if (!validation) {
if (validation.valid || validation.warnings.length === 0) { validation = await api.data.validateSalesData(formData.salesFile);
// Upload the file setUploadValidation(validation);
const uploadResult = await api.data.uploadSalesHistory( }
formData.salesFile,
{ tenant_id: currentTenantId } // FIXED: Check validation using correct field names
); if (!validation.is_valid) {
showNotification('success', 'Archivo subido', `${uploadResult.records_processed} registros procesados.`); const errorMessages = validation.errors.map(error =>
} else { `${error.row ? `Fila ${error.row}: ` : ''}${error.message}`
showNotification('warning', 'Datos con advertencias', 'Se encontraron algunas advertencias pero los datos son válidos.'); ).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) { } else if (currentStep === 4) {
// Start training using real training service // Training step
const trainingConfig = { const trainingConfig = {
include_weather: true, include_weather: true,
include_traffic: true, include_traffic: true,
@@ -245,7 +306,6 @@ const OnboardingPage = () => {
})); }));
showNotification('info', 'Entrenamiento iniciado', 'Los modelos se están entrenando...'); showNotification('info', 'Entrenamiento iniciado', 'Los modelos se están entrenando...');
// Don't move to next step automatically, wait for training completion
setLoading(false); setLoading(false);
return; return;
} }
@@ -289,7 +349,7 @@ const OnboardingPage = () => {
} }
}; };
const handleFileUpload = async (event) => { const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (file) { if (file) {
setFormData(prev => ({ ...prev, salesFile: file })); setFormData(prev => ({ ...prev, salesFile: file }));
@@ -298,33 +358,56 @@ const OnboardingPage = () => {
// Auto-validate file on selection // Auto-validate file on selection
try { try {
setLoading(true); setLoading(true);
console.log('Validating file:', file.name);
// FIXED: Use the corrected validation method
const validation = await api.data.validateSalesData(file); const validation = await api.data.validateSalesData(file);
setUploadValidation(validation); setUploadValidation(validation);
if (validation.valid) { // FIXED: Use correct field names from backend response
showNotification('success', 'Archivo válido', `${validation.recordCount} registros detectados.`); 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) { } 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 { } 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); 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 { } finally {
setLoading(false); 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 = () => ( const renderStepIndicator = () => (
<div className="mb-12"> <div className="mb-12">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">