diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte index 65ad9610..46bfb909 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte @@ -27,7 +27,7 @@ import ItemSheetInv from './inv/item-sheet-inv.svelte'; import { itemPresetsApi, type ItemPreset } from '$lib/api/dashboard/a76/item-presets'; import { Checkbox } from '$lib/components/ui/checkbox'; - import { cleanLineData, normalizeItemData } from '$lib/utils/items-logic'; + import { cleanLineData, normalizeItemData, formatItemError } from '$lib/utils/items-logic'; import { getVisibility } from '$lib/config/invoice-item-visibility'; let { @@ -370,19 +370,10 @@ await loadItems(); showItemSheet = false; } catch (error: any) { - const rawDetail = error?.response?.data?.detail ?? error?.response?.data ?? error?.message ?? String(error); - let errorMsg = 'No se pudo guardar la partida.'; - - if (Array.isArray(rawDetail)) { - errorMsg = rawDetail.map((e: any) => `[${e.loc?.join('.') ?? '?'}]: ${e.msg}`).join('\n'); - } else if (typeof rawDetail === 'string') { - errorMsg = rawDetail; - } else if (rawDetail && typeof rawDetail === 'object') { - errorMsg = JSON.stringify(rawDetail, null, 2); - } + const errorMsg = formatItemError(error); toast.error('Error al guardar', { - description: errorMsg.slice(0, 500), + description: errorMsg, duration: 10000 }); } finally { diff --git a/frontend/src/lib/utils/items-logic.ts b/frontend/src/lib/utils/items-logic.ts index 2b78ffc2..c48d2e1b 100644 --- a/frontend/src/lib/utils/items-logic.ts +++ b/frontend/src/lib/utils/items-logic.ts @@ -244,3 +244,73 @@ export function hasValues(obj: any): boolean { !(typeof val === 'object' && !hasValues(val)) ); } + +// Human-readable mapping for field errors +const FIELD_MAP: Record = { + 'class_id': 'Clase', + 'invoice_id': 'ID de Factura', + 'line_number': 'Número de Línea', + 'unit_of_measure': 'Unidad de Medida', + 'description.description_spanish': 'Descripción en Español', + 'description.description_english': 'Descripción en Inglés', + 'quantity.quantity': 'Cantidad', + 'quantity.net_weight': 'Peso Neto', + 'quantity.gross_weight': 'Peso Bruto', + 'financial.unit_cost_capture': 'Costo Unitario', + 'customs.fraction': 'Fracción Arancelaria', + 'customs.origin_country': 'País de Origen', + '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' +}; + +/** + * Formats a backend error into a human-readable Spanish message. + * Handles 422 (Validation), 403 (Forbidden), 404 (Not Found), and 500 (Server Error). + */ +export function formatItemError(error: any): string { + if (!error) return 'Error desconocido'; + + const status = error?.response?.status; + const data = error?.response?.data; + const detail = data?.detail; + + // 1. Handle Validation Errors (422) + if (status === 422 && Array.isArray(detail)) { + const errors = detail.map((err: any) => { + // Join location path, skipping 'body' prefix + const locPath = (err.loc || []) + .filter((l: string) => l !== 'body') + .join('.'); + + const fieldName = FIELD_MAP[locPath] || locPath || 'campo'; + + // Translate common Pydantic error messages + let msg = err.msg || 'error de validación'; + if (msg.includes('field required')) msg = 'es obligatorio'; + if (msg.includes('value is not a valid decimal')) msg = 'debe ser un número válido'; + if (msg.includes('value is not a valid integer')) msg = 'debe ser un número entero'; + + return `• ${fieldName}: ${msg}`; + }); + return `Por favor revisa los siguientes campos:\n${errors.join('\n')}`; + } + + // 2. Handle known detail strings (from raise HTTPException) + if (typeof detail === 'string') { + // Translate some common backend messages + if (detail.includes('Access denied')) return 'No tienes permisos para realizar esta acción o acceder a este recurso.'; + if (detail.includes('not found')) return 'El registro solicitado no existe o fue eliminado.'; + if (detail.includes('Class mismatch')) return 'Error de validación: ' + detail; // Keep descriptive logic errors + return detail; + } + + // 3. Handle Status Codes fallbacks + if (status === 403) return 'No tienes permisos para realizar esta operación.'; + if (status === 404) return 'No se encontró el recurso solicitado.'; + if (status >= 500) return 'Hubo un problema en el servidor. Por favor intenta de nuevo más tarde o contacta a soporte.'; + + // 4. Default Axios error message + return data?.message || error.message || 'Error al procesar la solicitud.'; +}