Add new frontend - fix 24

This commit is contained in:
Urtzi Alfaro
2025-07-23 17:51:39 +02:00
parent 20c31a07f7
commit e3a5256281
3 changed files with 161 additions and 313 deletions

View File

@@ -19,28 +19,13 @@ export interface UploadResponse {
upload_id?: string; upload_id?: string;
} }
// ✅ FIXED: Updated to match backend SalesValidationResult schema
export interface DataValidation { export interface DataValidation {
is_valid: boolean; // Changed from 'valid' to 'is_valid' valid: boolean; // ✅ Backend uses "valid", not "is_valid"
total_records: number; // Changed from 'recordCount' to 'total_records' errors: string[]; // ✅ Backend returns string array, not objects
valid_records: number; // Added missing field warnings: string[]; // ✅ Backend returns string array, not objects
invalid_records: number; // Added missing field suggestions: string[]; // ✅ Backend uses "suggestions", not "summary"
errors: Array<{ // Changed from string[] to object array recordCount?: number; // ✅ Optional field for record count (if backend provides it)
row?: number; duplicates?: number; // ✅ Optional field for duplicates
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
@@ -104,11 +89,10 @@ export class DataService {
* ✅ COMPLETELY 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, tenantId?: string): Promise<DataValidation> {
try { try {
console.log('Reading file content...', file.name); 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) { if (!fileContent) {
@@ -128,219 +112,92 @@ export class DataService {
} else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) { } else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
dataFormat = 'excel'; dataFormat = 'excel';
} else { } else {
// Default to CSV if unable to determine
dataFormat = 'csv'; dataFormat = 'csv';
} }
console.log('Detected file format:', dataFormat); console.log('Detected file format:', dataFormat);
// ✅ CRITICAL FIX: Use correct endpoint based on API Gateway routing // ✅ FIXED: Use proper tenant ID when available
const importData: SalesDataImportRequest = { const importData: SalesDataImportRequest = {
tenant_id: '', // Will be set by backend from auth context tenant_id: tenantId || '00000000-0000-0000-0000-000000000000',
data: fileContent, data: fileContent,
data_format: dataFormat, data_format: dataFormat,
validate_only: true validate_only: true
}; };
console.log('Sending validation request to:', '/api/v1/data/sales/import/validate'); console.log('Sending validation request with tenant_id:', importData.tenant_id);
// ✅ 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', // Gateway will proxy this to data service '/api/v1/data/sales/import/validate',
importData importData
); );
console.log('Raw response from API:', response); console.log('Raw response from API:', response);
// ✅ CRITICAL FIX: Handle various response formats properly // ✅ FIXED: Handle response according to backend's actual format
// Check if response contains error information first
if (response && typeof response === 'object') { if (response && typeof response === 'object') {
// Handle direct error responses (like 404) // Handle validation errors from FastAPI
if ('detail' in response) { if ('detail' in response) {
console.error('API returned error:', response.detail); console.error('API returned error:', response.detail);
if (response.detail === 'Not Found') { if (Array.isArray(response.detail)) {
// Return a proper validation failure for missing endpoint // Handle Pydantic validation errors
return { const errorMessages = response.detail.map(err => {
is_valid: false, if (typeof err === 'object' && err.msg) {
total_records: 0, return `${err.loc ? err.loc.join('.') + ': ' : ''}${err.msg}`;
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'
} }
}; return err.toString();
} else { });
// Handle other API errors
return { return {
is_valid: false, valid: false,
total_records: 0, errors: errorMessages,
valid_records: 0,
invalid_records: 0,
errors: [{
message: `Error del servidor: ${response.detail}`
}],
warnings: [], warnings: [],
summary: { suggestions: ['Revisa el formato de los datos enviados']
validation_error: true,
error_type: 'api_error',
message: response.detail
}
}; };
} }
return {
valid: false,
errors: [typeof response.detail === 'string' ? response.detail : 'Error de validación'],
warnings: [],
suggestions: []
};
} }
// Handle successful response (either wrapped or direct) // Handle successful response - check for nested data
const validationResult = response.data || response; if ('data' in response) {
return response.data;
// Verify that we have a proper validation response }
if (validationResult && typeof validationResult === 'object' && 'is_valid' in validationResult) {
console.log('Valid validation response received:', validationResult); // If response seems to be the validation result directly
return validationResult; if ('valid' in response) {
return response as DataValidation;
} }
} }
// If we get here, the response format is unexpected throw new Error('Invalid response format from validation service');
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) {
console.error('Error in validateSalesData:', error); console.error('Error validating file:', error);
// ✅ COMPREHENSIVE ERROR HANDLING 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;
}
// Handle network/connection errors // ✅ FIXED: Return format matching backend schema
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 de conexión. Verifica tu conexión a internet y vuelve a intentar.'
}],
warnings: [],
summary: {
validation_error: true,
error_type: 'network_error',
message: 'Failed to connect to validation service'
}
};
}
// 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 { return {
is_valid: false, valid: false,
total_records: 0, errors: [errorMessage],
valid_records: 0,
invalid_records: 0,
errors: [{
message: error.message || 'Error desconocido durante la validación'
}],
warnings: [], warnings: [],
summary: { suggestions: ['Intenta con un archivo diferente o contacta soporte']
validation_error: true,
error_type: 'unknown_error',
message: error.message || 'Unknown validation error'
}
}; };
} }
} }

View File

@@ -322,151 +322,140 @@ const OnboardingPage = () => {
} }
}; };
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => { // ✅ FIXED: Update handleFileUpload to use backend's schema
const file = event.target.files?.[0]; const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
if (!file) return; const file = event.target.files?.[0];
if (!file) return;
setFormData(prev => ({ ...prev, salesFile: file })); setFormData(prev => ({ ...prev, salesFile: file }));
setUploadValidation(null); setUploadValidation(null);
try {
setLoading(true);
console.log('Validating file:', file.name);
// Auto-validate file on selection // Pass the current tenant ID to validation
try { const validation = await api.data.validateSalesData(file, currentTenantId);
setLoading(true);
console.log('Validating file:', file.name); if (!validation) {
throw new Error('No validation response received from server');
// ✅ CRITICAL FIX: Add null check for validation response
const validation = await api.data.validateSalesData(file);
if (!validation) {
throw new Error('No validation response received from server');
}
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);
} }
};
console.log('Validation result:', validation);
setUploadValidation(validation);
// ✅ FIXED: Use backend's "valid" field instead of "is_valid"
if (validation.valid) {
showNotification('success', 'Archivo válido',
`${validation.recordCount || 'Algunos'} registros detectados.`);
} 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);
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 using backend's schema
setUploadValidation({
valid: false,
errors: [errorMessage],
warnings: [],
suggestions: ['Intenta con un archivo diferente']
});
} finally {
setLoading(false);
}
};
// Fixed validation display component // Fixed validation display component
const renderValidationResult = () => { const renderValidationResult = () => {
{uploadValidation && ( if (!uploadValidation) return null;
return (
<div className={`border rounded-lg p-4 ${ <div className={`border rounded-lg p-4 ${
uploadValidation.is_valid ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200' uploadValidation.valid ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'
}`}> }`}>
<div className="flex items-start"> <div className="flex items-start">
{uploadValidation.is_valid ? ( {uploadValidation.valid ? (
<CheckIcon className="w-5 h-5 text-green-600 mt-0.5 mr-3" /> <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" /> <XMarkIcon className="w-5 h-5 text-red-600 mt-0.5 mr-3" />
)} )}
<div className="flex-1"> <div className="flex-1">
<h4 className={`font-semibold ${ <h4 className={`font-semibold ${
uploadValidation.is_valid ? 'text-green-800' : 'text-red-800' uploadValidation.valid ? 'text-green-800' : 'text-red-800'
}`}> }`}>
{uploadValidation.is_valid ? 'Archivo válido' : 'Archivo con problemas'} {uploadValidation.valid ? 'Archivo válido' : 'Archivo con problemas'}
</h4> </h4>
{/* Always safe record count display */} {/* ✅ FIXED: Display record count using backend's field names */}
<p className={`text-sm mt-1 ${ <p className={`text-sm mt-1 ${
uploadValidation.is_valid ? 'text-green-700' : 'text-red-700' uploadValidation.valid ? 'text-green-700' : 'text-red-700'
}`}> }`}>
{uploadValidation.total_records || uploadValidation.recordCount || 0} registros procesados {uploadValidation.recordCount || 0} registros encontrados
{uploadValidation.duplicates && uploadValidation.duplicates > 0 &&
`, ${uploadValidation.duplicates} duplicados`}
</p> </p>
{/* Only show errors if they exist and are in array format */} {/* ✅ FIXED: Handle errors as string array (backend's current format) */}
{!uploadValidation.is_valid && uploadValidation.errors && ( {!uploadValidation.valid && uploadValidation.errors && uploadValidation.errors.length > 0 && (
<div className="mt-2"> <div className="mt-2">
<p className="text-sm font-medium text-red-700 mb-1"> <p className="text-sm font-medium text-red-700 mb-1">Errores encontrados:</p>
{Array.isArray(uploadValidation.errors) ? 'Errores encontrados:' : 'Error:'} <ul className="text-sm text-red-700 space-y-1">
</p> {uploadValidation.errors.map((error, idx) => (
{Array.isArray(uploadValidation.errors) ? ( <li key={idx}> {error}</li>
<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> </ul>
</div> </div>
)} )}
{/* Show technical info for debugging */} {/* ✅ FIXED: Handle warnings as string array (backend's current format) */}
{uploadValidation.summary && uploadValidation.summary.error_type && ( {uploadValidation.warnings && uploadValidation.warnings.length > 0 && (
<div className="mt-2 p-2 bg-gray-100 rounded"> <div className="mt-2">
<p className="text-xs text-gray-600"> <p className="text-sm font-medium text-yellow-700 mb-1">Advertencias:</p>
<strong>Info técnica:</strong> {uploadValidation.summary.error_type} <ul className="text-sm text-yellow-700 space-y-1">
</p> {uploadValidation.warnings.map((warning, idx) => (
<li key={idx}> {warning}</li>
))}
</ul>
</div>
)}
{/* ✅ FIXED: Handle backend's "suggestions" field */}
{uploadValidation.suggestions && uploadValidation.suggestions.length > 0 && (
<div className="mt-2">
<p className="text-sm font-medium text-blue-700 mb-1">Sugerencias:</p>
<ul className="text-sm text-blue-700 space-y-1">
{uploadValidation.suggestions.map((suggestion, idx) => (
<li key={idx}> {suggestion}</li>
))}
</ul>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div> </div>
)} );
}; };
const renderStepIndicator = () => ( const renderStepIndicator = () => (
<div className="mb-12"> <div className="mb-12">
@@ -814,8 +803,10 @@ const OnboardingPage = () => {
<div className="mt-2"> <div className="mt-2">
<p className="text-sm font-medium text-red-700 mb-1">Errores:</p> <p className="text-sm font-medium text-red-700 mb-1">Errores:</p>
<ul className="text-sm text-red-700 space-y-1"> <ul className="text-sm text-red-700 space-y-1">
{uploadValidation.errors.map((error, idx) => ( {validation.errors.map((error, idx) => (
<li key={idx}> {error}</li> <li key={idx}>
{typeof error === 'string' ? error : (error?.message || 'Error no especificado')}
</li>
))} ))}
</ul> </ul>
</div> </div>

View File

@@ -36,7 +36,7 @@ from shared.auth.decorators import (
get_current_tenant_id_dep get_current_tenant_id_dep
) )
router = APIRouter(prefix="/sales", tags=["sales"]) router = APIRouter(tags=["sales"])
logger = structlog.get_logger() logger = structlog.get_logger()
@router.post("/", response_model=SalesDataResponse) @router.post("/", response_model=SalesDataResponse)