diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 6e265dc7..31e47ba2 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -26,6 +26,65 @@ export function humanizeLineReferences(text: string): string { return text.replace(/\bline\[(\d+)\]/gi, 'partida $1'); } +function humanizeFieldPath(field: string): string { + const rawField = (field || '').trim(); + if (!rawField) return 'campo'; + + const lineMatch = rawField.match(/^line\[(\d+)\]\.(.+)$/i); + const fieldPath = lineMatch?.[2] || rawField; + const label = fieldPath + .replace(/^body\./i, '') + .replace(/\./g, ' → ') + .replace(/_/g, ' '); + + if (lineMatch) { + return `Partida ${lineMatch[1]} - ${label}`; + } + + return label; +} + +function humanizeValidationMessage(message: string): string { + const rawMessage = (message || '').trim(); + if (!rawMessage) return 'error de validación'; + + return rawMessage + .replace(/\b(field required|is required)\b/gi, 'es obligatorio') + .replace(/\b(value is not a valid decimal)\b/gi, 'debe ser un número válido') + .replace(/\b(value is not a valid integer)\b/gi, 'debe ser un número entero válido'); +} + +function formatValidationHint(field: string, message: string, code?: string): string { + const fieldLabel = humanizeFieldPath(field); + const normalizedMessage = humanizeValidationMessage(message); + + if (code === 'REQUIRED' || code === 'REQUIRED_FIELD' || /es obligatorio|es requerido/i.test(normalizedMessage)) { + return `Completa ${fieldLabel}.`; + } + + if (code === 'AMERICAN_FRACTION_NOT_FOUND') { + return 'La fracción americana seleccionada no existe. Elige una opción del catálogo.'; + } + + if (code === 'UNIT_OF_MEASURE_NOT_FOUND') { + return 'La unidad de medida seleccionada no existe. Elige una opción del catálogo.'; + } + + if (code === 'ORIGIN_COUNTRY_NOT_FOUND') { + return 'El país de origen seleccionado no existe. Elige una opción del catálogo.'; + } + + if (code === 'CLASS_NOT_FOUND') { + return 'La clase seleccionada no existe. Elige una opción del catálogo.'; + } + + if (code === 'FRACTION_TYPE_INVALID') { + return 'Selecciona un tipo de tarifa válido.'; + } + + return normalizedMessage; +} + /** * Título y descripción listos para toasts / alertas a partir de ApiResponse. * Prioriza los mensajes que ya envía el backend y evita duplicar rutas técnicas. @@ -34,7 +93,7 @@ export function friendlyApiErrorParts(res: ApiResponse): { title: string; descri const validationErrors = res.validationErrors; if (validationErrors?.length) { const blocks = validationErrors.map((e) => { - const base = humanizeLineReferences((e.message || '').trim() || e.field); + const base = formatValidationHint(e.field || '', e.message || '', e.code); const hints = e.solution?.filter(Boolean).length ? '\n' + e.solution!.map((s) => `• ${humanizeLineReferences(s)}`).join('\n') : ''; @@ -52,7 +111,7 @@ export function friendlyApiErrorParts(res: ApiResponse): { title: string; descri } if (res.error) { - const err = humanizeLineReferences(res.error.trim()); + const err = humanizeValidationMessage(humanizeLineReferences(res.error.trim())); if (err.startsWith('Error de validación:')) { return { title: 'Revisa los datos ingresados', @@ -241,6 +300,7 @@ async function fetchApi( if (response.status === 422) { // HTTPException(detail={ message, errors }) — catálogo / CSV parity const det = data.detail; + const validationErrors = (errors: unknown[]) => errors as NonNullable; if ( det && typeof det === 'object' && @@ -250,7 +310,7 @@ async function fetchApi( const d = det as { message?: string; errors: unknown[] }; return { error: d.message || 'Error de validación', - validationErrors: d.errors, + validationErrors: validationErrors(d.errors), status: response.status }; } @@ -258,7 +318,7 @@ async function fetchApi( if (data.errors && Array.isArray(data.errors)) { return { error: data.message || 'Error de validación', - validationErrors: data.errors, + validationErrors: validationErrors(data.errors), status: response.status }; } @@ -421,7 +481,7 @@ async function fetchApiFormDataPost( if (data.errors && Array.isArray(data.errors)) { resolve({ error: data.message || 'Error de validación', - validationErrors: data.errors, + validationErrors: data.errors as NonNullable, status: 422 }); return; @@ -431,8 +491,8 @@ async function fetchApiFormDataPost( if (Array.isArray(data.detail)) { const errors = data.detail .map((err: any) => { - const field = err.loc ? err.loc.join('.') : 'campo desconocido'; - return `${field}: ${err.msg}`; + const field = err.loc ? err.loc.filter((loc: string) => loc !== 'body').join('.') : 'campo desconocido'; + return `${humanizeFieldPath(field)}: ${humanizeValidationMessage(err.msg || 'error de validación')}`; }) .join(', '); errorMessage += errors; diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte index c2cdb76f..7a91f129 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte @@ -134,12 +134,12 @@
- +
- +
+

+ Los campos marcados con * son obligatorios. +

General @@ -272,7 +275,7 @@
- +
- + {#if editingItem?.quantity} {/if}
- +
- + {#if editingItem?.financial} {/if}
- +
- +
- + {#if editingItem?.customs}
- + {#if editingItem?.customs} {/if} diff --git a/frontend/src/lib/utils/items-logic.ts b/frontend/src/lib/utils/items-logic.ts index f9f0b4e5..f4caeb03 100644 --- a/frontend/src/lib/utils/items-logic.ts +++ b/frontend/src/lib/utils/items-logic.ts @@ -263,7 +263,28 @@ const FIELD_MAP: Record = { 'fa_data.search_invoice': 'Factura de Referencia', 'fa_data.search_line': 'Línea de Referencia', 'fa_data.search_type': 'Tipo de Búsqueda', - 'fa_data.movement_type_import': 'Tipo de Importación' + 'fa_data.movement_type_import': 'Tipo de Importación', + 'fa_data.is_subitem': 'Es Subpartida', + 'fa_data.subitem_number': 'Número de Partida Principal' +}; + +const FIELD_GUIDANCE: Record = { + class_id: 'Selecciona una clase.', + unit_of_measure: 'Selecciona una unidad de medida.', + 'quantity.quantity': 'Captura una cantidad válida mayor a cero.', + 'quantity.net_weight': 'Captura un peso neto válido mayor a cero.', + 'customs.fraction': 'Selecciona una fracción arancelaria válida.', + 'customs.origin_country': 'Selecciona un país de origen válido.', + 'customs.fraction_type': 'Selecciona un tipo de tarifa.', + 'customs.american_fraction': 'Selecciona una fracción americana válida.', + 'description.description_spanish': 'Captura la descripción en español.', + 'description.description_english': 'Captura la descripción en inglés.', + 'financial.unit_cost_capture': 'Captura un costo unitario válido.', + 'fa_data.search_invoice': 'Selecciona una factura de referencia.', + 'fa_data.search_line': 'Selecciona una línea de referencia.', + 'fa_data.search_type': 'Selecciona un tipo de búsqueda.', + 'fa_data.movement_type_import': 'Selecciona TEM o DEF.', + 'fa_data.subitem_number': 'Captura el número de la partida principal.' }; function humanizeFieldPath(field: string): string { @@ -295,6 +316,46 @@ function humanizeValidationMessage(message: string): string { .replace(/\b(value is not a valid integer)\b/gi, 'debe ser un número entero válido'); } +function formatFriendlyFieldMessage(fieldName: string, message: string, code?: string): string { + const cleanFieldName = fieldName.replace(/^Partida \d+ - /, ''); + const guidance = FIELD_GUIDANCE[cleanFieldName] || FIELD_GUIDANCE[fieldName]; + const normalizedMessage = humanizeValidationMessage(message); + + if (code === 'REQUIRED' || code === 'REQUIRED_FIELD' || /es requerido|es obligatorio/i.test(normalizedMessage)) { + return guidance || `Completa ${fieldName}.`; + } + + if (code === 'AMERICAN_FRACTION_NOT_FOUND') { + return `La fracción americana seleccionada no existe. Elige una opción del catálogo.`; + } + + if (code === 'FRACTION_TYPE_INVALID') { + return 'Selecciona un tipo de tarifa válido.'; + } + + if (code === 'UNIT_OF_MEASURE_NOT_FOUND') { + return 'La unidad de medida seleccionada no existe. Elige una opción del catálogo.'; + } + + if (code === 'ORIGIN_COUNTRY_NOT_FOUND') { + return 'El país de origen seleccionado no existe. Elige una opción del catálogo.'; + } + + if (code === 'CLASS_NOT_FOUND') { + return 'La clase seleccionada no existe. Elige una opción del catálogo.'; + } + + if (code === 'PACKAGE_NOT_FOUND' || code === 'PACKAGE_ID_REQUIRED') { + return 'El paquete seleccionado no es válido. Elige una opción del catálogo.'; + } + + if (code === 'MOVEMENT_TYPE_IMPORT_INVALID') { + return 'Selecciona TEM o DEF para el tipo de importación.'; + } + + return normalizedMessage; +} + /** * Formats a backend error into a human-readable Spanish message. * Handles 422 (Validation), 403 (Forbidden), 404 (Not Found), and 500 (Server Error). @@ -316,7 +377,7 @@ export function formatItemError(error: any): string { if (status === 422 && Array.isArray(validationErrors)) { const errors = validationErrors.map((err: any) => { const fieldName = humanizeFieldPath(err.field || ''); - const msg = humanizeValidationMessage(err.message || 'error de validación'); + const msg = formatFriendlyFieldMessage(fieldName, err.message || 'error de validación', err.code); return `• ${fieldName}: ${msg}`; }); @@ -332,7 +393,7 @@ export function formatItemError(error: any): string { .join('.'); const fieldName = humanizeFieldPath(locPath); - const msg = humanizeValidationMessage(err.msg || 'error de validación'); + const msg = formatFriendlyFieldMessage(fieldName, err.msg || 'error de validación', err.type); return `• ${fieldName}: ${msg}`; });