Add new frontend - fix 22
This commit is contained in:
@@ -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>>(
|
|
||||||
`/api/v1/data/sales/${id}`,
|
|
||||||
updates
|
|
||||||
);
|
|
||||||
return response.data!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Determine file format
|
||||||
* Delete sales record
|
const fileName = file.name.toLowerCase();
|
||||||
*/
|
let dataFormat: 'csv' | 'json' | 'excel';
|
||||||
async deleteSalesRecord(id: string): Promise<void> {
|
|
||||||
await apiClient.delete(`/api/v1/data/sales/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (fileName.endsWith('.csv')) {
|
||||||
* Get weather data
|
dataFormat = 'csv';
|
||||||
*/
|
} else if (fileName.endsWith('.json')) {
|
||||||
async getWeatherData(params?: {
|
dataFormat = 'json';
|
||||||
startDate?: string;
|
} else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
|
||||||
endDate?: string;
|
dataFormat = 'excel';
|
||||||
location?: string;
|
} else {
|
||||||
}): Promise<WeatherData[]> {
|
dataFormat = 'csv';
|
||||||
const response = await apiClient.get<ApiResponse<WeatherData[]>>(
|
}
|
||||||
'/api/v1/data/weather',
|
|
||||||
{ params }
|
|
||||||
);
|
|
||||||
return response.data!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const importData: SalesDataImportRequest = {
|
||||||
* Get traffic data
|
tenant_id: '', // Will be set by backend from auth context
|
||||||
*/
|
data: fileContent,
|
||||||
async getTrafficData(params?: {
|
data_format: dataFormat,
|
||||||
startDate?: string;
|
validate_only: false
|
||||||
endDate?: string;
|
};
|
||||||
location?: string;
|
|
||||||
}): Promise<TrafficData[]> {
|
|
||||||
const response = await apiClient.get<ApiResponse<TrafficData[]>>(
|
|
||||||
'/api/v1/data/traffic',
|
|
||||||
{ params }
|
|
||||||
);
|
|
||||||
return response.data!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const response = await apiClient.post<ApiResponse<UploadResponse>>(
|
||||||
* Get data quality report
|
'/api/v1/data/sales/import',
|
||||||
*/
|
importData
|
||||||
async getDataQuality(): Promise<{
|
);
|
||||||
salesData: { completeness: number; quality: number; lastUpdate: string };
|
|
||||||
weatherData: { completeness: number; quality: number; lastUpdate: string };
|
return response.data!;
|
||||||
trafficData: { completeness: number; quality: number; lastUpdate: string };
|
} catch (error: any) {
|
||||||
}> {
|
throw error;
|
||||||
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();
|
||||||
@@ -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) {
|
||||||
|
validation = await api.data.validateSalesData(formData.salesFile);
|
||||||
|
setUploadValidation(validation);
|
||||||
|
}
|
||||||
|
|
||||||
if (validation.valid || validation.warnings.length === 0) {
|
// FIXED: Check validation using correct field names
|
||||||
// Upload the file
|
if (!validation.is_valid) {
|
||||||
const uploadResult = await api.data.uploadSalesHistory(
|
const errorMessages = validation.errors.map(error =>
|
||||||
formData.salesFile,
|
`${error.row ? `Fila ${error.row}: ` : ''}${error.message}`
|
||||||
{ tenant_id: currentTenantId }
|
).join('; ');
|
||||||
);
|
|
||||||
showNotification('success', 'Archivo subido', `${uploadResult.records_processed} registros procesados.`);
|
showNotification('error', 'Datos inválidos',
|
||||||
} else {
|
`Se encontraron errores: ${errorMessages.slice(0, 200)}${errorMessages.length > 200 ? '...' : ''}`);
|
||||||
showNotification('warning', 'Datos con advertencias', 'Se encontraron algunas advertencias pero los datos son válidos.');
|
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">
|
||||||
|
|||||||
Reference in New Issue
Block a user