diff --git a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py index 4e3888ea..14420299 100644 --- a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py +++ b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py @@ -5,13 +5,15 @@ from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRat from api.v1.modules.a76.clients_and_providers.models import ClientProvider from api.v1.modules.public.reference_data.incoterms.models import Incoterm from api.v1.modules.a76.items.models import Item +from api.v1.modules.public.reference_data.currency_types.models import CurrencyType +from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection from ....models import TransportType, Currency, WeightUnit from core.exceptions import ErrorCollector def validate_common( db: Session, - invoice: schemas.InvoiceHeaderCreate, + invoice: schemas.InvoiceHeaderUpdate, tenant_id: int, company_id: int, errors: ErrorCollector, @@ -154,6 +156,18 @@ def validate_common( value=invoice.compliance_mx.remesa, ) else: + if not invoice.compliance_mx.is_pedimento_pending: + errors.add_error( + field="compliance_mx.pedimento_id", + message="El campo Pedimento es obligatorio cuando no se indica que el Pedimento está pendiente.", + solution=[ + "Proporciona un ID de Pedimento", + "Marca el campo Pedimento Pendiente", + ], + code="REQUIRED_FIELD", + value=invoice.compliance_mx.pedimento_id, + ) + if invoice.compliance_mx.remesa and not invoice.compliance_mx.pedimento_id: errors.add_error( field="compliance_mx.pedimento_id", @@ -185,7 +199,7 @@ def validate_common( if not exchange_rate_exists: errors.add_error( field="financials.exchange_rate", - message=f"No existe un Tipo de Cambio registrado para la fecha {invoice.invoice_date.date()}.", + message=f"No existe un Tipo de Cambio registrado para la fecha {invoice.invoice_date}.", solution=["Registra el Tipo de Cambio en el catálogo correspondiente"], code="EXCHANGE_RATE_NOT_FOUND", value=invoice.financials.exchange_rate, @@ -215,7 +229,7 @@ def validate_common( provider_exists = ( db.query(ClientProvider) .filter( - ClientProvider.id == invoice.provider_id, + ClientProvider.id == invoice.compliance_mx.provider_id, ClientProvider.tenant_id == tenant_id, ClientProvider.company_id == company_id, ) @@ -223,17 +237,17 @@ def validate_common( ) if not provider_exists: errors.add_error( - field="provider_id", + field="compliance_mx.provider_id", message="El Proveedor no existe en el Catálogo de Clientes y Proveedores.", solution=["Verifica el ID del Proveedor", "Revisa el catálogo"], code="NOT_FOUND", - value=invoice.provider_id, + value=invoice.compliance_mx.provider_id, ) selled_to_exists = ( db.query(ClientProvider) .filter( - ClientProvider.id == invoice.selled_to_id, + ClientProvider.id == invoice.compliance_mx.sold_to_id, ClientProvider.tenant_id == tenant_id, ClientProvider.company_id == company_id, ) @@ -241,17 +255,17 @@ def validate_common( ) if not selled_to_exists: errors.add_error( - field="selled_to_id", + field="compliance_mx.sold_to_id", message="El Cliente no existe en el Catálogo de Clientes y Proveedores.", solution=["Verifica el ID del Cliente", "Revisa el catálogo"], code="NOT_FOUND", - value=invoice.selled_to_id, + value=invoice.compliance_mx.sold_to_id, ) shipped_to_exists = ( db.query(ClientProvider) .filter( - ClientProvider.id == invoice.shipped_to_id, + ClientProvider.id == invoice.compliance_mx.shipped_to_id, ClientProvider.tenant_id == tenant_id, ClientProvider.company_id == company_id, ) @@ -259,17 +273,17 @@ def validate_common( ) if not shipped_to_exists: errors.add_error( - field="shipped_to_id", + field="compliance_mx.shipped_to_id", message="El Destinatario no existe en el Catálogo de Clientes y Proveedores.", solution=["Verifica el ID del Destinatario", "Revisa el catálogo"], code="NOT_FOUND", - value=invoice.shipped_to_id, + value=invoice.compliance_mx.shipped_to_id, ) customs_broker_exists = ( db.query(ClientProvider) .filter( - ClientProvider.id == invoice.customs_broker_id, + ClientProvider.id == invoice.compliance_mx.customs_broker_id, ClientProvider.tenant_id == tenant_id, ClientProvider.company_id == company_id, ) @@ -277,11 +291,11 @@ def validate_common( ) if not customs_broker_exists: errors.add_error( - field="customs_broker_id", + field="compliance_mx.customs_broker_id", message="El Agente Aduanal no existe en el Catálogo de Clientes y Proveedores.", solution=["Verifica el ID del Agente Aduanal", "Revisa el catálogo"], code="NOT_FOUND", - value=invoice.customs_broker_id, + value=invoice.compliance_mx.customs_broker_id, ) if invoice.logistics.carrier_id: @@ -314,16 +328,24 @@ def validate_common( value=invoice.logistics.transport_type, ) else: - if invoice.logistics.transport_type == "none" and invoice.logistics.transport_num: + if ( + invoice.logistics.transport_type == "none" + and invoice.logistics.transport_num + ): errors.add_error( field="logistics.transport_num", message="El Número de Transporte no debe proporcionarse cuando el Tipo de Transporte es 'none'.", - solution=["Elimina el Número de Transporte o selecciona un Tipo de Transporte válido"], + solution=[ + "Elimina el Número de Transporte o selecciona un Tipo de Transporte válido" + ], code="INVALID_VALUE", value=invoice.logistics.transport_num, ) else: - if not invoice.logistics.transport_num and invoice.logistics.transport_type != "none": + if ( + not invoice.logistics.transport_num + and invoice.logistics.transport_type != "none" + ): errors.add_error( field="logistics.transport_num", message="El Número de Transporte es obligatorio cuando se proporciona un Tipo de Transporte distinto de 'none'.", @@ -331,40 +353,72 @@ def validate_common( code="REQUIRED_FIELD", value=invoice.logistics.transport_num, ) - - - invoice.financials.currency = (invoice.financials.currency or "foreign") - + + invoice.financials.currency = invoice.financials.currency or "foreign" + if invoice.financials.currency not in [c.value for c in Currency]: errors.add_error( field="financials.currency", message="La Moneda proporcionada no es válida.", - solution=[ - f"Selecciona una Moneda válida: {[c.value for c in Currency]}" - ], + solution=[f"Selecciona una Moneda válida: {[c.value for c in Currency]}"], code="INVALID_CURRENCY", value=invoice.financials.currency, ) else: - has_items = db.query(Item).filter( - Item.invoice_id == invoice.id, - Item.tenant_id == tenant_id, - Item.company_id == company_id, - ).first() - if has_items: - errors.add_error( + # Only check for existing items during update operations (when invoice has an id) + if hasattr(invoice, "id"): + has_items = ( + db.query(Item) + .filter( + Item.invoice_id == invoice.id, + Item.tenant_id == tenant_id, + Item.company_id == company_id, + ) + .first() + ) + if has_items: + errors.add_error( field="items", - message=f"La opcion tipo de moneda {invoice.financials.currency} no puede ser modificada ya que la factura tiene items asociados.", - solution=["Verifica la moneda de los items asociados a la factura."], + message=f"La opcion tipo de moneda {invoice.financials.currency} no puede ser modificada ya que la factura tiene items asociados.", + solution=[ + "Verifica la moneda de los items asociados a la factura." + ], code="CURRENCY_CANNOT_BE_CHANGED", value=invoice.financials.currency, ) - - if invoice.logistics.incoterms: + + if invoice.financials.currency == "manual": + if not invoice.financials.currency_type: + errors.add_error( + field="financials.currency_type", + message="El Tipo de Moneda es obligatorio cuando la Moneda es 'manual'.", + solution=["Proporciona un Tipo de Moneda válido"], + code="REQUIRED_FIELD", + value=invoice.financials.currency_type, + ) + else: + currency_exists = ( + db.query(CurrencyType) + .filter(CurrencyType.code == invoice.financials.currency_type) + .first() + ) + if not currency_exists: + errors.add_error( + field="financials.currency_type", + message="El Tipo de Moneda no existe en el Catálogo de Tipos de Moneda.", + solution=[ + "Verifica el código del Tipo de Moneda", + "Revisa el catálogo", + ], + code="NOT_FOUND", + value=invoice.financials.currency_type, + ) + + if invoice.logistics.incoterm: incoterm_exists = ( db.query(Incoterm) .filter( - Incoterm.code == invoice.logistics.incoterms, + Incoterm.code == invoice.logistics.incoterm, Incoterm.tenant_id == tenant_id, Incoterm.company_id == company_id, ) @@ -372,13 +426,13 @@ def validate_common( ) if not incoterm_exists: errors.add_error( - field="logistics.incoterms", + field="logistics.incoterm", message="El Incoterm no existe en el Catálogo de Incoterms.", solution=["Verifica el código del Incoterm", "Revisa el catálogo"], code="NOT_FOUND", - value=invoice.logistics.incoterms, + value=invoice.logistics.incoterm, ) - + if invoice.logistics.weight_type not in [w.value for w in WeightUnit]: errors.add_error( field="logistics.weight_type", @@ -389,5 +443,20 @@ def validate_common( code="INVALID_WEIGHT_UNIT", value=invoice.logistics.weight_type, ) - - + + if invoice.compliance_mx.aduana: + custom_section_exists = ( + db.query(CustomsSection) + .filter( + CustomsSection.customs_code == invoice.compliance_mx.aduana, + ) + .first() + ) + if not custom_section_exists: + errors.add_error( + field="compliance_mx.aduana", + message="La Aduana no existe en el Catálogo de Secciones Aduaneras.", + solution=["Verifica el código de la Aduana", "Revisa el catálogo"], + code="NOT_FOUND", + value=invoice.compliance_mx.aduana, + ) diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py index 8c8a3a93..964d4e16 100644 --- a/backend/api/v1/modules/a76/invoices/schemas.py +++ b/backend/api/v1/modules/a76/invoices/schemas.py @@ -384,7 +384,7 @@ class InvoiceHeaderCreate(InvoiceHeaderBase): """Schema for creating Invoice Header with nested relations""" compliance_mx: Optional[InvoiceComplianceMxCreate] = None financials: Optional[InvoiceFinancialsCreate] = None - logistics: Optional[List[InvoiceLogisticsCreate]] = None + logistics: Optional[InvoiceLogisticsCreate] = None details: Optional[List[InvoiceSalesDetailsCreate]] = None collections: Optional[List[InvoiceCollectionsCreate]] = None @@ -418,9 +418,10 @@ class InvoiceCollectionsUpdate(InvoiceCollectionsBase): class InvoiceHeaderUpdate(InvoiceHeaderBase): """Schema for updating Invoice Header with nested relations""" + id: int compliance_mx: Optional[InvoiceComplianceMxUpdate] = None financials: Optional[InvoiceFinancialsUpdate] = None - logistics: Optional[List[InvoiceLogisticsUpdate]] = None + logistics: Optional[InvoiceLogisticsUpdate] = None details: Optional[List[InvoiceSalesDetailsUpdate]] = None collections: Optional[List[InvoiceCollectionsUpdate]] = None @@ -477,9 +478,9 @@ class InvoiceHeaderResponse(InvoiceHeaderBase): capture_date: datetime compliance_mx: Optional[InvoiceComplianceMxResponse] = None financials: Optional[InvoiceFinancialsResponse] = None - logistics: List[InvoiceLogisticsResponse] = [] - details: List[InvoiceSalesDetailsResponse] = [] - collections: List[InvoiceCollectionsResponse] = [] + logistics: Optional[InvoiceLogisticsResponse] = [] + details: Optional[InvoiceSalesDetailsResponse] = [] + collections: Optional[InvoiceCollectionsResponse] = [] class Config: from_attributes = True diff --git a/backend/api/v1/modules/a76/invoices/services.py b/backend/api/v1/modules/a76/invoices/services.py index e911ed9f..07a5fab9 100644 --- a/backend/api/v1/modules/a76/invoices/services.py +++ b/backend/api/v1/modules/a76/invoices/services.py @@ -90,8 +90,8 @@ class InvoiceService: errors = ErrorCollector() # Validar si la factura ya existe - #invoice_exists(db, invoice_data.invoice_number, tenant_id, company_id, errors) - #validate_create(db, invoice_data, tenant_id, company_id, errors) + invoice_exists(db, invoice_data.invoice_number, tenant_id, company_id, errors) + validate_create(db, invoice_data, tenant_id, company_id, errors) # Si hay errores, lanzar excepción errors.raise_if_errors("Error al crear la factura") diff --git a/backend/core/error_handlers.py b/backend/core/error_handlers.py index 91f49ee1..f07687b7 100644 --- a/backend/core/error_handlers.py +++ b/backend/core/error_handlers.py @@ -31,6 +31,10 @@ async def base_exception_handler( }, ) + # Log detailed errors if they exist + if hasattr(exc, "errors") and exc.errors: + logger.warning(f"Validation errors details: {exc.errors}") + return JSONResponse( status_code=exc.status_code, content=exc.to_dict(), diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 5959b930..5d863da1 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -10,6 +10,13 @@ const API_BASE_URL = (import.meta.env.VITE_API_URL || '').replace(/\/+$/, ''); export interface ApiResponse { data?: T; error?: string; + validationErrors?: Array<{ + field: string; + message: string; + code?: string; + solution?: string[]; + value?: any; + }>; status: number; } @@ -207,30 +214,41 @@ async function fetchApi( if (!response.ok) { // Manejo especial para errores 422 (validation error) - if (response.status === 422 && data.detail) { - let errorMessage = 'Error de validación: '; - - // FastAPI devuelve errores de validación en data.detail como array - 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}`; - }).join(', '); - errorMessage += errors; - } else if (typeof data.detail === 'string') { - errorMessage = data.detail; - } else { - errorMessage += JSON.stringify(data.detail); + if (response.status === 422) { + // Errores de validación personalizados (con array errors) + if (data.errors && Array.isArray(data.errors)) { + return { + error: data.message || 'Error de validación', + validationErrors: data.errors, + status: response.status + }; + } + // Errores de validación de FastAPI (con detail) + else if (data.detail) { + let errorMessage = 'Error de validación: '; + + // FastAPI devuelve errores de validación en data.detail como array + 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}`; + }).join(', '); + errorMessage += errors; + } else if (typeof data.detail === 'string') { + errorMessage = data.detail; + } else { + errorMessage += JSON.stringify(data.detail); + } + + return { + error: errorMessage, + status: response.status + }; } - - return { - error: errorMessage, - status: response.status - }; } return { - error: data.detail || 'Error en la petición', + error: data.message || data.detail || 'Error en la petición', status: response.status }; } diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index 4af6238b..4ae71bfb 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -276,7 +276,7 @@ export interface CreateInvoiceData { enajenation_goods?: boolean | null; compliance_mx?: Omit | null; financials?: Omit | null; - logistics?: Omit[] | null; + logistics?: Omit | null; details?: Omit[] | null; collections?: Omit[] | null; } diff --git a/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte index b5aa7eed..6c42b0ac 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte @@ -11,20 +11,16 @@ let { invoice, formData = $bindable(), - invoiceTypes = [], customsBrokers = [], clients = [], providers = [], currencyTypes = [], - transportTypes = [], transporters = [], vehicles = [], drivers = [], trailers = [], customsSections = [], codePedimentoRegimens = [], - defaultOperationType = undefined, - defaultInvoiceType = undefined, operationType = undefined, exchangeRate = undefined }: { @@ -76,7 +72,7 @@ weight_type: 'kgs', iva_factor: invoice.financials?.iva_factor || null, carrier_id: invoice.logistics?.[0]?.carrier_id || null, - transport_id: '', + transport_id: invoice.logistics?.[0]?.transport_id || '', driver_name: invoice.logistics?.[0]?.driver_name || '', transport_type: invoice.logistics?.[0]?.transport_type || '', transport_num: invoice.logistics?.[0]?.vehicle_num || '', @@ -133,6 +129,21 @@ { value: 'lbs', label: 'Libras (lb)' } ]; + // Opciones de tipo de transporte + const transportTypeOptions = [ + { value: 'none', label: 'Ninguno' }, + { value: 'transport', label: 'Transporte' }, + { value: 'box', label: 'Caja' }, + { value: 'licence_plates', label: 'Placas' }, + { value: 'truck', label: 'Camión' }, + { value: 'vessel', label: 'Buque' }, + { value: 'rail_barge', label: 'Ferrobarcaza' }, + { value: 'container', label: 'Contenedor' }, + { value: 'airplane', label: 'Avión' }, + { value: 'gondola', label: 'Góndola' }, + { value: 'flatbed', label: 'Plataforma' } + ]; + // Opciones de encabezados const providerHeaderOptions = [ { value: 'proveedor', label: 'Proveedor' }, @@ -540,18 +551,18 @@
- + { - formData.transport_type = v ?? ''; + formData.transport_id = v ?? ''; }} > - + - {#if formData.transport_type} - {vehicles.find(v => v.vehicle_key === formData.transport_type)?.vehicle_key || formData.transport_type} + {#if formData.transport_id} + {vehicles.find(v => v.vehicle_key === formData.transport_id)?.vehicle_key || formData.transport_id} {:else if vehicles.length > 0} Selecciona vehículo... {:else} @@ -605,28 +616,22 @@ { - formData.transport_id = v ?? 'Ninguno'; + formData.transport_type = v ?? 'none'; }} > - + - {formData.transport_id || 'Ninguno'} + {transportTypeOptions.find(t => t.value === formData.transport_type)?.label || 'Ninguno'} - Ninguno - Transporte - Caja - Placas - Camión - Buque - Ferrobarcaza - Contenedor - Avion - Gondola - Plataforma + {#each transportTypeOptions as option} + + {option.label} + + {/each}
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts index 88f965d2..a254ab2e 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts +++ b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts @@ -17,7 +17,20 @@ interface SaveInvoiceOptions { formData: FormDataSet; } -export async function saveInvoice(options: SaveInvoiceOptions): Promise<{ success: boolean; error?: string; newInvoiceId?: number }> { +interface SaveInvoiceResult { + success: boolean; + error?: string; + newInvoiceId?: number; + validationErrors?: Array<{ + field: string; + message: string; + code?: string; + solution?: string[]; + value?: any; + }>; +} + +export async function saveInvoice(options: SaveInvoiceOptions): Promise { const { invoiceId, isCreate, companyId, formData } = options; const { InvoiceTopFieldsFormData, generalFormData, observationFormData, itemsFormData, othersFormData, continuationFormData } = formData; @@ -61,9 +74,13 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise<{ succes if (isCreate) { // Crear nueva factura con todos sus sub-recursos const response = await invoicesApi.create(companyId, payload as CreateInvoiceData); + console.log('Create response:', response); if (response.error) { const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura'; - throw new Error(errorMsg); + const error: any = new Error(errorMsg); + error.validationErrors = response.validationErrors; + console.log('Throwing error with validationErrors:', error.validationErrors); + throw error; } if (!response.data?.id) throw new Error('No se recibió el ID de la factura creada'); newInvoiceId = response.data.id; @@ -73,13 +90,22 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise<{ succes } else { // Actualizar factura existente con todos sus sub-recursos const response = await invoicesApi.update(invoiceId!, companyId, payload as UpdateInvoiceData); - if (response.error) throw new Error(response.error); + console.log('Update response:', response); + if (response.error) { + const error: any = new Error(response.error); + error.validationErrors = response.validationErrors; + console.log('Throwing error with validationErrors:', error.validationErrors); + throw error; + } } return { success: true, newInvoiceId: newInvoiceId ?? undefined }; } catch (e) { + console.log('Caught error in saveInvoice:', e); + console.log('Error has validationErrors?', (e as any)?.validationErrors); const error = e instanceof Error ? e.message : 'Error al guardar los cambios'; - return { success: false, error }; + const validationErrors = (e as any)?.validationErrors; + return { success: false, error, validationErrors }; } } @@ -136,14 +162,7 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI } // Logistics - const hasLogisticsFromGeneral = generalFormData?.carrier_id || - generalFormData?.driver_name || generalFormData?.transport_type || generalFormData?.transport_num; - - const hasLogisticsFromObservations = observationFormData?.incoterm; - - if (hasLogisticsFromGeneral || hasLogisticsFromObservations) { - payload.logistics = buildLogisticsData(generalFormData, observationFormData); - } + payload.logistics = buildLogisticsData(generalFormData, observationFormData); // Eliminar campos undefined para no enviarlos Object.keys(payload).forEach(key => { @@ -206,12 +225,12 @@ function buildFinancialsData(generalFormData: any, observationFormData: any, oth } function buildLogisticsData(generalFormData: any, observationFormData: any) { - // Crear un solo entry de logistics con los datos de generalFormData y observationFormData - return [{ + // Retornar logistics como objeto único + return { carrier_id: generalFormData?.carrier_id || null, - transport_type: generalFormData?.transport_type || null, + transport_type: generalFormData?.transport_type || 'none', driver_name: generalFormData?.driver_name || null, vehicle_num: generalFormData?.transport_num || null, incoterm: observationFormData?.incoterm || null, - }]; + }; } diff --git a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte index b8580283..4db580c2 100644 --- a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte @@ -144,13 +144,23 @@ } }); + console.log('Save result:', result); + if (!result.success) { - throw new Error(result.error || 'Error al guardar la factura'); + // Crear un error con validationErrors si existen + const error: any = new Error(result.error || 'Error al guardar la factura'); + if (result.validationErrors) { + error.validationErrors = result.validationErrors; + } + throw error; } toast.success('Todos los cambios se guardaron correctamente'); } catch (e) { console.error('Error saving all:', e); + console.log('Error object:', e); + console.log('Has validationErrors?', (e as any)?.validationErrors); + if (e instanceof Error && e.message.includes('401')) { toast.error('Sesión expirada. Recargando página...'); setTimeout(() => { @@ -158,9 +168,23 @@ }, 1500); } else { const errorMessage = e instanceof Error ? e.message : 'Error al guardar los cambios'; - toast.error(errorMessage, { - description: 'Revisa la consola para más detalles' - }); + + // Si hay errores de validación, mostrarlos en detalle + if (e && typeof e === 'object' && 'validationErrors' in e && Array.isArray((e as any).validationErrors)) { + const validationErrors = (e as any).validationErrors; + console.log('Found validationErrors:', validationErrors); + const errorList = validationErrors.map((err: any) => + `• ${err.field}: ${err.message}${err.solution ? ' - ' + err.solution.join(', ') : ''}` + ).join('\n'); + + toast.error(errorMessage, { + description: errorList + }); + } else { + toast.error(errorMessage, { + description: 'Revisa la consola para más detalles' + }); + } } } finally { saving = false;