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 { apiClient } from '../base/apiClient';
|
||||||
import { ApiResponse } from '../types/api';
|
import { ApiResponse } from '../types/api';
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export interface UploadResponse {
|
|||||||
upload_id?: string;
|
upload_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXED: Updated to match backend SalesValidationResult schema
|
// ✅ FIXED: Updated to match backend SalesValidationResult schema
|
||||||
export interface DataValidation {
|
export interface DataValidation {
|
||||||
is_valid: boolean; // Changed from 'valid' to 'is_valid'
|
is_valid: boolean; // Changed from 'valid' to 'is_valid'
|
||||||
total_records: number; // Changed from 'recordCount' to 'total_records'
|
total_records: number; // Changed from 'recordCount' to 'total_records'
|
||||||
@@ -75,7 +75,7 @@ export interface CreateSalesRequest {
|
|||||||
date: string;
|
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 {
|
export interface SalesDataImportRequest {
|
||||||
tenant_id: string;
|
tenant_id: string;
|
||||||
data: string; // File content as 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
|
* Backend expects JSON data with SalesDataImport structure, not a file upload
|
||||||
*/
|
*/
|
||||||
async validateSalesData(file: File): Promise<DataValidation> {
|
async validateSalesData(file: File): Promise<DataValidation> {
|
||||||
try {
|
try {
|
||||||
// Read file content
|
console.log('Reading file content...', file.name);
|
||||||
|
|
||||||
|
// ✅ FIXED: Proper file reading implementation
|
||||||
const fileContent = await this.readFileAsText(file);
|
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 from extension
|
||||||
const fileName = file.name.toLowerCase();
|
const fileName = file.name.toLowerCase();
|
||||||
let dataFormat: 'csv' | 'json' | 'excel';
|
let dataFormat: 'csv' | 'json' | 'excel';
|
||||||
@@ -124,7 +132,9 @@ export class DataService {
|
|||||||
dataFormat = 'csv';
|
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 = {
|
const importData: SalesDataImportRequest = {
|
||||||
tenant_id: '', // Will be set by backend from auth context
|
tenant_id: '', // Will be set by backend from auth context
|
||||||
data: fileContent,
|
data: fileContent,
|
||||||
@@ -132,47 +142,234 @@ export class DataService {
|
|||||||
validate_only: true
|
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>>(
|
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
|
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) {
|
} catch (error: any) {
|
||||||
// Handle validation errors gracefully
|
console.error('Error in validateSalesData:', error);
|
||||||
if (error.response?.status === 422) {
|
|
||||||
// Return a failed validation result instead of throwing
|
// ✅ COMPREHENSIVE ERROR HANDLING
|
||||||
|
|
||||||
|
// Handle network/connection errors
|
||||||
|
if (error.code === 'NETWORK_ERROR' || error.message?.includes('fetch')) {
|
||||||
return {
|
return {
|
||||||
is_valid: false,
|
is_valid: false,
|
||||||
total_records: 0,
|
total_records: 0,
|
||||||
valid_records: 0,
|
valid_records: 0,
|
||||||
invalid_records: 0,
|
invalid_records: 0,
|
||||||
errors: [{
|
errors: [{
|
||||||
message: error.response?.data?.detail || 'Validation failed'
|
message: 'Error de conexión. Verifica tu conexión a internet y vuelve a intentar.'
|
||||||
}],
|
}],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
summary: {
|
summary: {
|
||||||
validation_error: true,
|
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> {
|
private readFileAsText(file: File): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (event) => {
|
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 = () => {
|
reader.onerror = () => {
|
||||||
reject(new Error('Failed to read file'));
|
reject(new Error('Failed to read file'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
reader.onabort = () => {
|
||||||
|
reject(new Error('File reading was aborted'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read the file as text
|
||||||
reader.readAsText(file);
|
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> {
|
async updateSalesRecord(id: string, record: Partial<CreateSalesRequest>): Promise<SalesRecord> {
|
||||||
try {
|
const response = await apiClient.put<ApiResponse<SalesRecord>>(
|
||||||
const fileContent = await this.readFileAsText(file);
|
`/api/v1/data/sales/${id}`,
|
||||||
|
record
|
||||||
// Determine file format
|
);
|
||||||
const fileName = file.name.toLowerCase();
|
return response.data!;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
format?: 'csv' | 'excel';
|
page?: number;
|
||||||
}): Promise<Blob> {
|
limit?: number;
|
||||||
const response = await apiClient.get('/api/v1/data/sales/export', {
|
}): Promise<{ data: WeatherData[]; total: number; page: number; pages: number }> {
|
||||||
params,
|
const response = await apiClient.get<ApiResponse<{
|
||||||
responseType: 'blob',
|
data: WeatherData[];
|
||||||
});
|
total: number;
|
||||||
return response as unknown as Blob;
|
page: number;
|
||||||
}
|
pages: number;
|
||||||
|
}>>('/api/v1/data/weather', { params });
|
||||||
/**
|
|
||||||
* Get product list
|
|
||||||
*/
|
|
||||||
async getProducts(): Promise<string[]> {
|
|
||||||
const response = await apiClient.get<ApiResponse<string[]>>('/api/v1/data/products');
|
|
||||||
return response.data!;
|
return response.data!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get data sync status
|
* Get traffic data
|
||||||
*/
|
*/
|
||||||
async getSyncStatus(): Promise<{
|
async getTrafficData(params?: {
|
||||||
weather: { lastSync: string; status: 'ok' | 'error'; nextSync: string };
|
startDate?: string;
|
||||||
traffic: { lastSync: string; status: 'ok' | 'error'; nextSync: string };
|
endDate?: string;
|
||||||
}> {
|
page?: number;
|
||||||
const response = await apiClient.get<ApiResponse<any>>('/api/v1/data/sync/status');
|
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!;
|
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();
|
export const dataService = new DataService();
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
} 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';
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ 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) {
|
||||||
// FIXED: Sales upload step with proper validation handling
|
// FIXED: Sales upload step with proper validation handling
|
||||||
if (formData.salesFile) {
|
if (formData.salesFile) {
|
||||||
try {
|
try {
|
||||||
@@ -219,10 +219,12 @@ const OnboardingPage = () => {
|
|||||||
setUploadValidation(validation);
|
setUploadValidation(validation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXED: Check validation using correct field names
|
// ✅ FIXED: Check validation using correct field name is_valid
|
||||||
if (!validation.is_valid) {
|
if (!validation.is_valid) {
|
||||||
const errorMessages = validation.errors.map(error =>
|
const errorMessages = validation.errors.map(error =>
|
||||||
`${error.row ? `Fila ${error.row}: ` : ''}${error.message}`
|
`${error.row ? `Fila ${error.row}: ` : ''}${
|
||||||
|
typeof error === 'string' ? error : error.message
|
||||||
|
}`
|
||||||
).join('; ');
|
).join('; ');
|
||||||
|
|
||||||
showNotification('error', 'Datos inválidos',
|
showNotification('error', 'Datos inválidos',
|
||||||
@@ -234,15 +236,16 @@ const OnboardingPage = () => {
|
|||||||
// Show warnings if any
|
// Show warnings if any
|
||||||
if (validation.warnings.length > 0) {
|
if (validation.warnings.length > 0) {
|
||||||
const warningMessages = validation.warnings.map(warning =>
|
const warningMessages = validation.warnings.map(warning =>
|
||||||
`${warning.row ? `Fila ${warning.row}: ` : ''}${warning.message}`
|
`${warning.row ? `Fila ${warning.row}: ` : ''}${
|
||||||
|
typeof warning === 'string' ? warning : warning.message
|
||||||
|
}`
|
||||||
).join('; ');
|
).join('; ');
|
||||||
|
|
||||||
showNotification('warning', 'Advertencias encontradas',
|
showNotification('warning', 'Advertencias encontradas',
|
||||||
`Advertencias: ${warningMessages.slice(0, 200)}${warningMessages.length > 200 ? '...' : ''}`);
|
`Advertencias: ${warningMessages.slice(0, 200)}${warningMessages.length > 200 ? '...' : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed with import - Use the existing uploadSalesHistory method
|
// Proceed with actual upload
|
||||||
// or create a new importSalesData method
|
|
||||||
const uploadResult = await api.data.uploadSalesHistory(
|
const uploadResult = await api.data.uploadSalesHistory(
|
||||||
formData.salesFile,
|
formData.salesFile,
|
||||||
{ tenant_id: currentTenantId }
|
{ tenant_id: currentTenantId }
|
||||||
@@ -251,39 +254,9 @@ const OnboardingPage = () => {
|
|||||||
showNotification('success', 'Archivo subido',
|
showNotification('success', 'Archivo subido',
|
||||||
`${uploadResult.records_processed} registros procesados exitosamente.`);
|
`${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) {
|
} catch (error: any) {
|
||||||
console.error('Sales upload error:', error);
|
console.error('Sales upload error:', error);
|
||||||
|
// ... existing error handling
|
||||||
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) {
|
||||||
@@ -351,63 +324,150 @@ const OnboardingPage = () => {
|
|||||||
|
|
||||||
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file) {
|
if (!file) return;
|
||||||
setFormData(prev => ({ ...prev, salesFile: file }));
|
|
||||||
setUploadValidation(null);
|
|
||||||
|
|
||||||
// Auto-validate file on selection
|
setFormData(prev => ({ ...prev, salesFile: file }));
|
||||||
try {
|
setUploadValidation(null);
|
||||||
setLoading(true);
|
|
||||||
console.log('Validating file:', file.name);
|
|
||||||
|
|
||||||
// FIXED: Use the corrected validation method
|
// Auto-validate file on selection
|
||||||
const validation = await api.data.validateSalesData(file);
|
try {
|
||||||
setUploadValidation(validation);
|
setLoading(true);
|
||||||
|
console.log('Validating file:', file.name);
|
||||||
|
|
||||||
// FIXED: Use correct field names from backend response
|
// ✅ CRITICAL FIX: Add null check for validation response
|
||||||
if (validation.is_valid) {
|
const validation = await api.data.validateSalesData(file);
|
||||||
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.');
|
|
||||||
} else {
|
|
||||||
const errorCount = validation.errors.length;
|
|
||||||
showNotification('error', 'Archivo con errores',
|
|
||||||
`Se encontraron ${errorCount} errores en el archivo.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error: any) {
|
if (!validation) {
|
||||||
console.error('Error validating file:', error);
|
throw new Error('No validation response received from server');
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Validation result:', validation); // Debug log
|
||||||
|
|
||||||
|
setUploadValidation(validation);
|
||||||
|
|
||||||
|
// ✅ FIXED: Use correct field name is_valid instead of valid
|
||||||
|
if (validation.is_valid) {
|
||||||
|
showNotification('success', 'Archivo válido',
|
||||||
|
`${validation.total_records} registros detectados, ${validation.valid_records} válidos.`);
|
||||||
|
} else if (validation.warnings && validation.warnings.length > 0 &&
|
||||||
|
validation.errors && validation.errors.length === 0) {
|
||||||
|
showNotification('warning', 'Archivo con advertencias',
|
||||||
|
'El archivo es válido pero tiene algunas advertencias.');
|
||||||
|
} else {
|
||||||
|
const errorCount = validation.errors ? validation.errors.length : 0;
|
||||||
|
showNotification('error', 'Archivo con errores',
|
||||||
|
`Se encontraron ${errorCount} errores en el archivo.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error validating file:', error);
|
||||||
|
|
||||||
|
// 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.response?.status === 500) {
|
||||||
|
errorMessage = 'Error del servidor. Inténtalo más tarde.';
|
||||||
|
} else if (error.message) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification('error', 'Error de validación', errorMessage);
|
||||||
|
|
||||||
|
// ✅ FIXED: Set validation state with correct field names
|
||||||
|
setUploadValidation({
|
||||||
|
is_valid: false, // ✅ Use is_valid not valid
|
||||||
|
total_records: 0,
|
||||||
|
valid_records: 0,
|
||||||
|
invalid_records: 0,
|
||||||
|
errors: [{ message: errorMessage }], // ✅ Array of objects, not strings
|
||||||
|
warnings: [],
|
||||||
|
summary: { validation_failed: true }
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fixed validation display component
|
||||||
|
const renderValidationResult = () => {
|
||||||
|
{uploadValidation && (
|
||||||
|
<div className={`border rounded-lg p-4 ${
|
||||||
|
uploadValidation.is_valid ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-start">
|
||||||
|
{uploadValidation.is_valid ? (
|
||||||
|
<CheckIcon className="w-5 h-5 text-green-600 mt-0.5 mr-3" />
|
||||||
|
) : (
|
||||||
|
<XMarkIcon className="w-5 h-5 text-red-600 mt-0.5 mr-3" />
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className={`font-semibold ${
|
||||||
|
uploadValidation.is_valid ? 'text-green-800' : 'text-red-800'
|
||||||
|
}`}>
|
||||||
|
{uploadValidation.is_valid ? 'Archivo válido' : 'Archivo con problemas'}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* Always safe record count display */}
|
||||||
|
<p className={`text-sm mt-1 ${
|
||||||
|
uploadValidation.is_valid ? 'text-green-700' : 'text-red-700'
|
||||||
|
}`}>
|
||||||
|
{uploadValidation.total_records || uploadValidation.recordCount || 0} registros procesados
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Only show errors if they exist and are in array format */}
|
||||||
|
{!uploadValidation.is_valid && uploadValidation.errors && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm font-medium text-red-700 mb-1">
|
||||||
|
{Array.isArray(uploadValidation.errors) ? 'Errores encontrados:' : 'Error:'}
|
||||||
|
</p>
|
||||||
|
{Array.isArray(uploadValidation.errors) ? (
|
||||||
|
<ul className="text-sm text-red-700 space-y-1">
|
||||||
|
{uploadValidation.errors.map((error, idx) => (
|
||||||
|
<li key={idx}>
|
||||||
|
• {error?.message || error || 'Error no especificado'}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-red-700">
|
||||||
|
{uploadValidation.errors.message || uploadValidation.errors || 'Error desconocido'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Show warnings if they exist */}
|
||||||
|
{uploadValidation.warnings && Array.isArray(uploadValidation.warnings) && uploadValidation.warnings.length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm font-medium text-yellow-700 mb-1">Advertencias:</p>
|
||||||
|
<ul className="text-sm text-yellow-700 space-y-1">
|
||||||
|
{uploadValidation.warnings.map((warning, idx) => (
|
||||||
|
<li key={idx}>
|
||||||
|
• {warning?.message || warning || 'Advertencia no especificada'}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Show technical info for debugging */}
|
||||||
|
{uploadValidation.summary && uploadValidation.summary.error_type && (
|
||||||
|
<div className="mt-2 p-2 bg-gray-100 rounded">
|
||||||
|
<p className="text-xs text-gray-600">
|
||||||
|
<strong>Info técnica:</strong> {uploadValidation.summary.error_type}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
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