Add new frontend - fix 23
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// frontend/src/api/services/dataService.ts - COMPLETE FIX
|
||||
// frontend/src/api/services/dataService.ts - COMPLETE WORKING FIX
|
||||
import { apiClient } from '../base/apiClient';
|
||||
import { ApiResponse } from '../types/api';
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface UploadResponse {
|
||||
upload_id?: string;
|
||||
}
|
||||
|
||||
// FIXED: Updated to match backend SalesValidationResult schema
|
||||
// ✅ FIXED: Updated to match backend SalesValidationResult schema
|
||||
export interface DataValidation {
|
||||
is_valid: boolean; // Changed from 'valid' to 'is_valid'
|
||||
total_records: number; // Changed from 'recordCount' to 'total_records'
|
||||
@@ -75,7 +75,7 @@ export interface CreateSalesRequest {
|
||||
date: string;
|
||||
}
|
||||
|
||||
// FIXED: Interface for import data that matches backend SalesDataImport schema
|
||||
// ✅ FIXED: Interface for import data that matches backend SalesDataImport schema
|
||||
export interface SalesDataImportRequest {
|
||||
tenant_id: string;
|
||||
data: string; // File content as string
|
||||
@@ -101,14 +101,22 @@ export class DataService {
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXED: Validate sales data before upload
|
||||
* ✅ COMPLETELY FIXED: Validate sales data before upload
|
||||
* Backend expects JSON data with SalesDataImport structure, not a file upload
|
||||
*/
|
||||
async validateSalesData(file: File): Promise<DataValidation> {
|
||||
try {
|
||||
// Read file content
|
||||
console.log('Reading file content...', file.name);
|
||||
|
||||
// ✅ FIXED: Proper file reading implementation
|
||||
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';
|
||||
@@ -124,7 +132,9 @@ export class DataService {
|
||||
dataFormat = 'csv';
|
||||
}
|
||||
|
||||
// FIXED: Use the correct endpoint and send JSON data instead of file upload
|
||||
console.log('Detected file format:', dataFormat);
|
||||
|
||||
// ✅ CRITICAL FIX: Use correct endpoint based on API Gateway routing
|
||||
const importData: SalesDataImportRequest = {
|
||||
tenant_id: '', // Will be set by backend from auth context
|
||||
data: fileContent,
|
||||
@@ -132,47 +142,234 @@ export class DataService {
|
||||
validate_only: true
|
||||
};
|
||||
|
||||
console.log('Sending validation request to:', '/api/v1/data/sales/import/validate');
|
||||
|
||||
// ✅ FIXED: Correct endpoint path - Gateway routes /api/v1/data/sales/* to data service /api/v1/sales/*
|
||||
const response = await apiClient.post<ApiResponse<DataValidation>>(
|
||||
'/api/v1/data/sales/import/validate', // Fixed endpoint path
|
||||
'/api/v1/data/sales/import/validate', // Gateway will proxy this to data service
|
||||
importData
|
||||
);
|
||||
|
||||
return response.data!;
|
||||
console.log('Raw response from API:', response);
|
||||
|
||||
// ✅ CRITICAL FIX: Handle various response formats properly
|
||||
// Check if response contains error information first
|
||||
if (response && typeof response === 'object') {
|
||||
// Handle direct error responses (like 404)
|
||||
if ('detail' in response) {
|
||||
console.error('API returned error:', response.detail);
|
||||
|
||||
if (response.detail === 'Not Found') {
|
||||
// Return a proper validation failure for missing endpoint
|
||||
return {
|
||||
is_valid: false,
|
||||
total_records: 0,
|
||||
valid_records: 0,
|
||||
invalid_records: 0,
|
||||
errors: [{
|
||||
message: 'El servicio de validación no está disponible temporalmente. Por favor contacta al soporte técnico.'
|
||||
}],
|
||||
warnings: [],
|
||||
summary: {
|
||||
validation_error: true,
|
||||
error_type: 'service_unavailable',
|
||||
message: 'Validation service endpoint not found'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Handle other API errors
|
||||
return {
|
||||
is_valid: false,
|
||||
total_records: 0,
|
||||
valid_records: 0,
|
||||
invalid_records: 0,
|
||||
errors: [{
|
||||
message: `Error del servidor: ${response.detail}`
|
||||
}],
|
||||
warnings: [],
|
||||
summary: {
|
||||
validation_error: true,
|
||||
error_type: 'api_error',
|
||||
message: response.detail
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle successful response (either wrapped or direct)
|
||||
const validationResult = response.data || response;
|
||||
|
||||
// Verify that we have a proper validation response
|
||||
if (validationResult && typeof validationResult === 'object' && 'is_valid' in validationResult) {
|
||||
console.log('Valid validation response received:', validationResult);
|
||||
return validationResult;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, the response format is unexpected
|
||||
console.warn('Unexpected response format:', response);
|
||||
return {
|
||||
is_valid: false,
|
||||
total_records: 0,
|
||||
valid_records: 0,
|
||||
invalid_records: 0,
|
||||
errors: [{
|
||||
message: 'Respuesta inválida del servidor. Formato de respuesta no reconocido.'
|
||||
}],
|
||||
warnings: [],
|
||||
summary: {
|
||||
validation_error: true,
|
||||
error_type: 'invalid_response',
|
||||
message: 'Unexpected response format from validation service'
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
// Handle validation errors gracefully
|
||||
if (error.response?.status === 422) {
|
||||
// Return a failed validation result instead of throwing
|
||||
console.error('Error in validateSalesData:', error);
|
||||
|
||||
// ✅ COMPREHENSIVE ERROR HANDLING
|
||||
|
||||
// Handle network/connection errors
|
||||
if (error.code === 'NETWORK_ERROR' || error.message?.includes('fetch')) {
|
||||
return {
|
||||
is_valid: false,
|
||||
total_records: 0,
|
||||
valid_records: 0,
|
||||
invalid_records: 0,
|
||||
errors: [{
|
||||
message: error.response?.data?.detail || 'Validation failed'
|
||||
message: 'Error de conexión. Verifica tu conexión a internet y vuelve a intentar.'
|
||||
}],
|
||||
warnings: [],
|
||||
summary: {
|
||||
validation_error: true,
|
||||
message: error.response?.data?.detail || 'File validation failed'
|
||||
error_type: 'network_error',
|
||||
message: 'Failed to connect to validation service'
|
||||
}
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
|
||||
// Handle HTTP status errors
|
||||
if (error.response) {
|
||||
const status = error.response.status;
|
||||
console.log('HTTP error status:', status);
|
||||
|
||||
if (status === 404) {
|
||||
return {
|
||||
is_valid: false,
|
||||
total_records: 0,
|
||||
valid_records: 0,
|
||||
invalid_records: 0,
|
||||
errors: [{
|
||||
message: 'El servicio de validación no está disponible. Contacta al administrador del sistema.'
|
||||
}],
|
||||
warnings: [],
|
||||
summary: {
|
||||
validation_error: true,
|
||||
error_type: 'service_not_found',
|
||||
message: 'Validation endpoint not found (404)'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 422) {
|
||||
return {
|
||||
is_valid: false,
|
||||
total_records: 0,
|
||||
valid_records: 0,
|
||||
invalid_records: 0,
|
||||
errors: [{
|
||||
message: error.response?.data?.detail || 'Formato de datos inválido'
|
||||
}],
|
||||
warnings: [],
|
||||
summary: {
|
||||
validation_error: true,
|
||||
error_type: 'validation_failed',
|
||||
message: 'Data validation failed'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 400) {
|
||||
return {
|
||||
is_valid: false,
|
||||
total_records: 0,
|
||||
valid_records: 0,
|
||||
invalid_records: 0,
|
||||
errors: [{
|
||||
message: error.response?.data?.detail || 'Solicitud inválida'
|
||||
}],
|
||||
warnings: [],
|
||||
summary: {
|
||||
validation_error: true,
|
||||
error_type: 'bad_request',
|
||||
message: 'Bad request to validation service'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (status >= 500) {
|
||||
return {
|
||||
is_valid: false,
|
||||
total_records: 0,
|
||||
valid_records: 0,
|
||||
invalid_records: 0,
|
||||
errors: [{
|
||||
message: 'Error interno del servidor. Inténtalo más tarde.'
|
||||
}],
|
||||
warnings: [],
|
||||
summary: {
|
||||
validation_error: true,
|
||||
error_type: 'server_error',
|
||||
message: 'Internal server error during validation'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle any other unknown errors
|
||||
return {
|
||||
is_valid: false,
|
||||
total_records: 0,
|
||||
valid_records: 0,
|
||||
invalid_records: 0,
|
||||
errors: [{
|
||||
message: error.message || 'Error desconocido durante la validación'
|
||||
}],
|
||||
warnings: [],
|
||||
summary: {
|
||||
validation_error: true,
|
||||
error_type: 'unknown_error',
|
||||
message: error.message || 'Unknown validation error'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to read file as text
|
||||
* ✅ FIXED: Proper helper method to read file as text with error handling
|
||||
*/
|
||||
private readFileAsText(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (event) => {
|
||||
resolve(event.target?.result as string);
|
||||
const result = event.target?.result;
|
||||
if (typeof result === 'string') {
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(new Error('Failed to read file as text'));
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error('Failed to read file'));
|
||||
};
|
||||
|
||||
reader.onabort = () => {
|
||||
reject(new Error('File reading was aborted'));
|
||||
};
|
||||
|
||||
// Read the file as text
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
@@ -218,85 +415,59 @@ export class DataService {
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXED: Import sales data (actual import after validation)
|
||||
* Update sales record
|
||||
*/
|
||||
async importSalesData(file: File): Promise<UploadResponse> {
|
||||
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';
|
||||
}
|
||||
|
||||
const importData: SalesDataImportRequest = {
|
||||
tenant_id: '', // Will be set by backend from auth context
|
||||
data: fileContent,
|
||||
data_format: dataFormat,
|
||||
validate_only: false
|
||||
};
|
||||
|
||||
const response = await apiClient.post<ApiResponse<UploadResponse>>(
|
||||
'/api/v1/data/sales/import',
|
||||
importData
|
||||
);
|
||||
|
||||
return response.data!;
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
async updateSalesRecord(id: string, record: Partial<CreateSalesRequest>): Promise<SalesRecord> {
|
||||
const response = await apiClient.put<ApiResponse<SalesRecord>>(
|
||||
`/api/v1/data/sales/${id}`,
|
||||
record
|
||||
);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export sales data
|
||||
* Delete sales record
|
||||
*/
|
||||
async exportSalesData(params?: {
|
||||
async deleteSalesRecord(id: string): Promise<void> {
|
||||
await apiClient.delete(`/api/v1/data/sales/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get weather data
|
||||
*/
|
||||
async getWeatherData(params?: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
format?: 'csv' | 'excel';
|
||||
}): Promise<Blob> {
|
||||
const response = await apiClient.get('/api/v1/data/sales/export', {
|
||||
params,
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response as unknown as Blob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product list
|
||||
*/
|
||||
async getProducts(): Promise<string[]> {
|
||||
const response = await apiClient.get<ApiResponse<string[]>>('/api/v1/data/products');
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): Promise<{ data: WeatherData[]; total: number; page: number; pages: number }> {
|
||||
const response = await apiClient.get<ApiResponse<{
|
||||
data: WeatherData[];
|
||||
total: number;
|
||||
page: number;
|
||||
pages: number;
|
||||
}>>('/api/v1/data/weather', { params });
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data sync status
|
||||
* Get traffic data
|
||||
*/
|
||||
async getSyncStatus(): Promise<{
|
||||
weather: { lastSync: string; status: 'ok' | 'error'; nextSync: string };
|
||||
traffic: { lastSync: string; status: 'ok' | 'error'; nextSync: string };
|
||||
}> {
|
||||
const response = await apiClient.get<ApiResponse<any>>('/api/v1/data/sync/status');
|
||||
async getTrafficData(params?: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): Promise<{ data: TrafficData[]; total: number; page: number; pages: number }> {
|
||||
const response = await apiClient.get<ApiResponse<{
|
||||
data: TrafficData[];
|
||||
total: number;
|
||||
page: number;
|
||||
pages: number;
|
||||
}>>('/api/v1/data/traffic', { params });
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger manual data sync
|
||||
*/
|
||||
async triggerSync(dataType: 'weather' | 'traffic' | 'all'): Promise<void> {
|
||||
await apiClient.post('/api/v1/data/sync/trigger', { data_type: dataType });
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Export the instance with the name expected by the index file
|
||||
// ✅ CRITICAL FIX: Export the instance that index.ts expects
|
||||
export const dataService = new DataService();
|
||||
Reference in New Issue
Block a user