Add new frontend - fix 25
This commit is contained in:
@@ -20,12 +20,35 @@ export interface UploadResponse {
|
||||
}
|
||||
|
||||
export interface DataValidation {
|
||||
valid: boolean; // ✅ Backend uses "valid", not "is_valid"
|
||||
errors: string[]; // ✅ Backend returns string array, not objects
|
||||
warnings: string[]; // ✅ Backend returns string array, not objects
|
||||
suggestions: string[]; // ✅ Backend uses "suggestions", not "summary"
|
||||
recordCount?: number; // ✅ Optional field for record count (if backend provides it)
|
||||
duplicates?: number; // ✅ Optional field for duplicates
|
||||
// ✅ NEW: Backend SalesValidationResult schema fields
|
||||
is_valid: boolean;
|
||||
total_records: number;
|
||||
valid_records: number;
|
||||
invalid_records: number;
|
||||
errors: Array<{
|
||||
type: string;
|
||||
message: string;
|
||||
field?: string;
|
||||
row?: number;
|
||||
code?: string;
|
||||
}>;
|
||||
warnings: Array<{
|
||||
type: string;
|
||||
message: string;
|
||||
field?: string;
|
||||
row?: number;
|
||||
code?: string;
|
||||
}>;
|
||||
summary: {
|
||||
status: string;
|
||||
file_format?: string;
|
||||
file_size_bytes?: number;
|
||||
file_size_mb?: number;
|
||||
estimated_processing_time_seconds?: number;
|
||||
validation_timestamp?: string;
|
||||
suggestions: string[];
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
// Data types
|
||||
@@ -71,37 +94,139 @@ export interface SalesDataImportRequest {
|
||||
|
||||
export class DataService {
|
||||
/**
|
||||
* Upload sales history file
|
||||
* ✅ FIXED: Upload sales history file to the correct backend endpoint
|
||||
* Backend expects: UploadFile + Form data at /api/v1/data/sales/import
|
||||
*/
|
||||
async uploadSalesHistory(
|
||||
file: File,
|
||||
tenantId?: string,
|
||||
additionalData?: Record<string, any>
|
||||
): Promise<UploadResponse> {
|
||||
const response = await apiClient.upload<ApiResponse<UploadResponse>>(
|
||||
'/api/v1/data/upload-sales',
|
||||
file,
|
||||
additionalData
|
||||
);
|
||||
return response.data!;
|
||||
try {
|
||||
console.log('Uploading sales file:', file.name);
|
||||
|
||||
// ✅ CRITICAL FIX: Use the correct endpoint that exists in backend
|
||||
// Backend endpoint: @router.post("/import", response_model=SalesImportResult)
|
||||
// Full path: /api/v1/data/sales/import (mounted with prefix /api/v1/sales)
|
||||
|
||||
// Determine file format
|
||||
const fileName = file.name.toLowerCase();
|
||||
let fileFormat: string;
|
||||
|
||||
if (fileName.endsWith('.csv')) {
|
||||
fileFormat = 'csv';
|
||||
} else if (fileName.endsWith('.json')) {
|
||||
fileFormat = 'json';
|
||||
} else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
|
||||
fileFormat = 'excel';
|
||||
} else {
|
||||
fileFormat = 'csv'; // Default fallback
|
||||
}
|
||||
|
||||
// ✅ FIXED: Create FormData manually to match backend expectations
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('file_format', fileFormat);
|
||||
|
||||
if (tenantId) {
|
||||
formData.append('tenant_id', tenantId);
|
||||
}
|
||||
|
||||
// Add additional data if provided
|
||||
if (additionalData) {
|
||||
Object.entries(additionalData).forEach(([key, value]) => {
|
||||
formData.append(key, String(value));
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Uploading with file_format:', fileFormat);
|
||||
|
||||
// ✅ FIXED: Use the correct endpoint that exists in the backend
|
||||
const response = await apiClient.request<ApiResponse<any>>(
|
||||
'/api/v1/data/sales/import', // Correct endpoint path
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
// Don't set Content-Type header - let browser set it with boundary
|
||||
headers: {} // Empty headers to avoid setting Content-Type manually
|
||||
}
|
||||
);
|
||||
|
||||
console.log('Upload response:', response);
|
||||
|
||||
// ✅ Handle the SalesImportResult response structure
|
||||
if (response && typeof response === 'object') {
|
||||
// Handle API errors
|
||||
if ('detail' in response) {
|
||||
throw new Error(typeof response.detail === 'string' ? response.detail : 'Upload failed');
|
||||
}
|
||||
|
||||
// Extract data from response
|
||||
let uploadResult: any;
|
||||
if ('data' in response && response.data) {
|
||||
uploadResult = response.data;
|
||||
} else {
|
||||
uploadResult = response;
|
||||
}
|
||||
|
||||
// ✅ FIXED: Map backend SalesImportResult to frontend UploadResponse
|
||||
return {
|
||||
message: uploadResult.success
|
||||
? `Successfully processed ${uploadResult.records_created || uploadResult.records_processed || 0} records`
|
||||
: 'Upload completed with issues',
|
||||
records_processed: uploadResult.records_created || uploadResult.records_processed || 0,
|
||||
errors: uploadResult.errors ?
|
||||
(Array.isArray(uploadResult.errors) ?
|
||||
uploadResult.errors.map((err: any) =>
|
||||
typeof err === 'string' ? err : (err.message || String(err))
|
||||
) : [String(uploadResult.errors)]
|
||||
) : [],
|
||||
upload_id: uploadResult.id || undefined
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Invalid response format from upload service');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error uploading file:', error);
|
||||
|
||||
let errorMessage = 'Error al subir 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.response?.status === 500) {
|
||||
errorMessage = 'Error del servidor. Inténtalo más tarde.';
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
// Throw structured error that can be caught by the frontend
|
||||
throw {
|
||||
message: errorMessage,
|
||||
status: error.response?.status || 0,
|
||||
code: error.code,
|
||||
details: error.response?.data || {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Alternative method: Upload using the import JSON endpoint instead of file upload
|
||||
/**
|
||||
* ✅ COMPLETELY FIXED: Validate sales data before upload
|
||||
* Backend expects JSON data with SalesDataImport structure, not a file upload
|
||||
* ✅ ALTERNATIVE: Upload sales data using the JSON import endpoint
|
||||
* This uses the same endpoint as validation but with validate_only: false
|
||||
*/
|
||||
async validateSalesData(file: File, tenantId?: string): Promise<DataValidation> {
|
||||
async uploadSalesDataAsJson(file: File, tenantId?: string): Promise<UploadResponse> {
|
||||
try {
|
||||
console.log('Reading file content...', file.name);
|
||||
console.log('Uploading sales data as JSON:', file.name);
|
||||
|
||||
const fileContent = await this.readFileAsText(file);
|
||||
|
||||
if (!fileContent) {
|
||||
throw new Error('Failed to read file content');
|
||||
}
|
||||
|
||||
console.log('File content read successfully, length:', fileContent.length);
|
||||
|
||||
// Determine file format from extension
|
||||
// Determine file format
|
||||
const fileName = file.name.toLowerCase();
|
||||
let dataFormat: 'csv' | 'json' | 'excel';
|
||||
|
||||
@@ -114,94 +239,249 @@ export class DataService {
|
||||
} else {
|
||||
dataFormat = 'csv';
|
||||
}
|
||||
|
||||
console.log('Detected file format:', dataFormat);
|
||||
|
||||
// ✅ FIXED: Use proper tenant ID when available
|
||||
|
||||
// ✅ Use the same structure as validation but with validate_only: false
|
||||
const importData: SalesDataImportRequest = {
|
||||
tenant_id: tenantId || '00000000-0000-0000-0000-000000000000',
|
||||
data: fileContent,
|
||||
data_format: dataFormat,
|
||||
validate_only: true
|
||||
validate_only: false, // This makes it actually import the data
|
||||
source: 'onboarding_upload'
|
||||
};
|
||||
|
||||
console.log('Sending validation request with tenant_id:', importData.tenant_id);
|
||||
|
||||
const response = await apiClient.post<ApiResponse<DataValidation>>(
|
||||
'/api/v1/data/sales/import/validate',
|
||||
|
||||
console.log('Uploading data with validate_only: false');
|
||||
|
||||
// ✅ OPTION: Add a new JSON import endpoint to the backend
|
||||
const response = await apiClient.post<ApiResponse<any>>(
|
||||
'/api/v1/data/sales/import/json', // Need to add this endpoint to backend
|
||||
importData
|
||||
);
|
||||
|
||||
console.log('Raw response from API:', response);
|
||||
|
||||
// ✅ FIXED: Handle response according to backend's actual format
|
||||
console.log('JSON upload response:', response);
|
||||
|
||||
// Handle response similar to file upload
|
||||
if (response && typeof response === 'object') {
|
||||
// Handle validation errors from FastAPI
|
||||
if ('detail' in response) {
|
||||
console.error('API returned error:', response.detail);
|
||||
|
||||
if (Array.isArray(response.detail)) {
|
||||
// Handle Pydantic validation errors
|
||||
const errorMessages = response.detail.map(err => {
|
||||
if (typeof err === 'object' && err.msg) {
|
||||
return `${err.loc ? err.loc.join('.') + ': ' : ''}${err.msg}`;
|
||||
}
|
||||
return err.toString();
|
||||
});
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
errors: errorMessages,
|
||||
warnings: [],
|
||||
suggestions: ['Revisa el formato de los datos enviados']
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
errors: [typeof response.detail === 'string' ? response.detail : 'Error de validación'],
|
||||
warnings: [],
|
||||
suggestions: []
|
||||
};
|
||||
throw new Error(typeof response.detail === 'string' ? response.detail : 'Upload failed');
|
||||
}
|
||||
|
||||
// Handle successful response - check for nested data
|
||||
if ('data' in response) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// If response seems to be the validation result directly
|
||||
if ('valid' in response) {
|
||||
return response as DataValidation;
|
||||
|
||||
let uploadResult: any;
|
||||
if ('data' in response && response.data) {
|
||||
uploadResult = response.data;
|
||||
} else {
|
||||
uploadResult = response;
|
||||
}
|
||||
|
||||
return {
|
||||
message: uploadResult.success
|
||||
? `Successfully processed ${uploadResult.records_created || uploadResult.records_processed || 0} records`
|
||||
: 'Upload completed with issues',
|
||||
records_processed: uploadResult.records_created || uploadResult.records_processed || 0,
|
||||
errors: uploadResult.errors ?
|
||||
(Array.isArray(uploadResult.errors) ?
|
||||
uploadResult.errors.map((err: any) =>
|
||||
typeof err === 'string' ? err : (err.message || String(err))
|
||||
) : [String(uploadResult.errors)]
|
||||
) : [],
|
||||
upload_id: uploadResult.id || undefined
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Invalid response format from validation service');
|
||||
|
||||
throw new Error('Invalid response format from upload service');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error validating file:', error);
|
||||
console.error('Error uploading JSON data:', error);
|
||||
|
||||
let errorMessage = 'Error al validar el archivo';
|
||||
let errorMessage = 'Error al subir los datos';
|
||||
if (error.response?.status === 422) {
|
||||
errorMessage = 'Formato de archivo inválido';
|
||||
errorMessage = 'Formato de datos inválido';
|
||||
} else if (error.response?.status === 400) {
|
||||
errorMessage = 'El archivo no se puede procesar';
|
||||
errorMessage = 'Los datos no se pueden procesar';
|
||||
} else if (error.response?.status === 500) {
|
||||
errorMessage = 'Error del servidor. Inténtalo más tarde.';
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
// ✅ FIXED: Return format matching backend schema
|
||||
return {
|
||||
valid: false,
|
||||
errors: [errorMessage],
|
||||
warnings: [],
|
||||
suggestions: ['Intenta con un archivo diferente o contacta soporte']
|
||||
throw {
|
||||
message: errorMessage,
|
||||
status: error.response?.status || 0,
|
||||
code: error.code,
|
||||
details: error.response?.data || {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async validateSalesData(file: File, tenantId?: string): Promise<DataValidation> {
|
||||
try {
|
||||
console.log('Reading file content...', file.name);
|
||||
|
||||
const fileContent = await this.readFileAsText(file);
|
||||
|
||||
if (!fileContent) {
|
||||
throw new Error('Failed to read file content');
|
||||
}
|
||||
|
||||
console.log('File content read successfully, length:', fileContent.length);
|
||||
|
||||
// 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 {
|
||||
dataFormat = 'csv'; // Default fallback
|
||||
}
|
||||
|
||||
console.log('Detected file format:', dataFormat);
|
||||
|
||||
// ✅ FIXED: Use proper tenant ID when available
|
||||
const importData: SalesDataImportRequest = {
|
||||
tenant_id: tenantId || '00000000-0000-0000-0000-000000000000',
|
||||
data: fileContent,
|
||||
data_format: dataFormat,
|
||||
validate_only: true
|
||||
};
|
||||
|
||||
console.log('Sending validation request with tenant_id:', importData.tenant_id);
|
||||
|
||||
const response = await apiClient.post<ApiResponse<DataValidation>>(
|
||||
'/api/v1/data/sales/import/validate',
|
||||
importData
|
||||
);
|
||||
|
||||
console.log('Raw response from API:', response);
|
||||
|
||||
// ✅ ENHANCED: Handle the new backend response structure
|
||||
if (response && typeof response === 'object') {
|
||||
// Handle API errors
|
||||
if ('detail' in response) {
|
||||
console.error('API returned error:', response.detail);
|
||||
|
||||
if (Array.isArray(response.detail)) {
|
||||
// Handle Pydantic validation errors
|
||||
const errorMessages = response.detail.map(err => ({
|
||||
type: 'pydantic_error',
|
||||
message: `${err.loc ? err.loc.join('.') + ': ' : ''}${err.msg}`,
|
||||
field: err.loc ? err.loc[err.loc.length - 1] : null,
|
||||
code: err.type
|
||||
}));
|
||||
|
||||
return {
|
||||
is_valid: false,
|
||||
total_records: 0,
|
||||
valid_records: 0,
|
||||
invalid_records: 0,
|
||||
errors: errorMessages,
|
||||
warnings: [],
|
||||
summary: {
|
||||
status: 'error',
|
||||
suggestions: ['Revisa el formato de los datos enviados']
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle simple error messages
|
||||
return {
|
||||
is_valid: false,
|
||||
total_records: 0,
|
||||
valid_records: 0,
|
||||
invalid_records: 0,
|
||||
errors: [{
|
||||
type: 'api_error',
|
||||
message: typeof response.detail === 'string' ? response.detail : 'Error de validación',
|
||||
code: 'API_ERROR'
|
||||
}],
|
||||
warnings: [],
|
||||
summary: {
|
||||
status: 'error',
|
||||
suggestions: ['Verifica el archivo y vuelve a intentar']
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ SUCCESS: Handle successful validation response
|
||||
let validationResult: DataValidation;
|
||||
|
||||
// Check if response has nested data
|
||||
if ('data' in response && response.data) {
|
||||
validationResult = response.data;
|
||||
} else if ('is_valid' in response) {
|
||||
// Direct response
|
||||
validationResult = response as DataValidation;
|
||||
} else {
|
||||
throw new Error('Invalid response format from validation service');
|
||||
}
|
||||
|
||||
// ✅ ENHANCED: Normalize the response to ensure all required fields exist
|
||||
return {
|
||||
is_valid: validationResult.is_valid,
|
||||
total_records: validationResult.total_records || 0,
|
||||
valid_records: validationResult.valid_records || 0,
|
||||
invalid_records: validationResult.invalid_records || 0,
|
||||
errors: validationResult.errors || [],
|
||||
warnings: validationResult.warnings || [],
|
||||
summary: validationResult.summary || { status: 'unknown', suggestions: [] },
|
||||
|
||||
// Backward compatibility fields
|
||||
valid: validationResult.is_valid, // Map for legacy code
|
||||
recordCount: validationResult.total_records,
|
||||
suggestions: validationResult.summary?.suggestions || []
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Invalid response format from validation service');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error validating file:', error);
|
||||
|
||||
let errorMessage = 'Error al validar el archivo';
|
||||
let errorCode = 'UNKNOWN_ERROR';
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
errorMessage = 'Formato de archivo inválido';
|
||||
errorCode = 'INVALID_FORMAT';
|
||||
} else if (error.response?.status === 400) {
|
||||
errorMessage = 'El archivo no se puede procesar';
|
||||
errorCode = 'PROCESSING_ERROR';
|
||||
} else if (error.response?.status === 500) {
|
||||
errorMessage = 'Error del servidor. Inténtalo más tarde.';
|
||||
errorCode = 'SERVER_ERROR';
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
errorCode = 'CLIENT_ERROR';
|
||||
}
|
||||
|
||||
// Return properly structured error response matching new schema
|
||||
return {
|
||||
is_valid: false,
|
||||
total_records: 0,
|
||||
valid_records: 0,
|
||||
invalid_records: 0,
|
||||
errors: [{
|
||||
type: 'client_error',
|
||||
message: errorMessage,
|
||||
code: errorCode
|
||||
}],
|
||||
warnings: [],
|
||||
summary: {
|
||||
status: 'error',
|
||||
suggestions: ['Intenta con un archivo diferente o contacta soporte']
|
||||
},
|
||||
|
||||
// Backward compatibility
|
||||
valid: false,
|
||||
recordCount: 0,
|
||||
suggestions: ['Intenta con un archivo diferente o contacta soporte']
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ FIXED: Proper helper method to read file as text with error handling
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user