diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py index 92f4d00f..702988b0 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py @@ -207,13 +207,16 @@ class PedimentosService: Pedimentos.year == pedimento_data.year, Pedimentos.customs_office == pedimento_data.customs_office, Pedimentos.license == pedimento_data.license, - Pedimentos.pedimento_number == pedimento_data.pedimento_number, - Pedimentos.deleted_at.is_(None) + Pedimentos.pedimento_number == pedimento_data.pedimento_number ).first() if existing: + if existing.deleted_at: + raise ValueError( + f"Ya existe un pedimento con estos datos ({pedimento_data.year}-{pedimento_data.customs_office}-{pedimento_data.license}-{pedimento_data.pedimento_number}) pero está en la papelera. Debes restaurarlo o usar otro número." + ) raise ValueError( - f"Ya existe un pedimento con estos datos: {pedimento_data.year}-{pedimento_data.customs_office}-{pedimento_data.license}-{pedimento_data.pedimento_number}" + f"Ya existe un pedimento registrado con estos datos: {pedimento_data.year}-{pedimento_data.customs_office}-{pedimento_data.license}-{pedimento_data.pedimento_number}" ) # Extraer datos de tablas relacionadas diff --git a/backend/core/error_handlers.py b/backend/core/error_handlers.py index c3c4c90c..dd3e49db 100644 --- a/backend/core/error_handlers.py +++ b/backend/core/error_handlers.py @@ -112,7 +112,7 @@ async def integrity_error_handler( orig_msg = str(exc.orig).lower() # Check for unique/duplicate key violations (English and Spanish) - if any(kw in orig_msg for kw in ["unique constraint", "duplicate key", "duplicada", "unicidad", "ya existe"]): + if any(kw in orig_msg for kw in ["unique constraint", "duplicate key", "duplicada", "unicidad", "ya existe", "llave duplicada", "pedimentos_unique_key"]): error_message = "El registro ya existe. Verifica los campos únicos (Año, Aduana, Patente, Número, etc.)." # Check for foreign key violations (English and Spanish) elif any(kw in orig_msg for kw in ["foreign key", "foránea", "referencia"]): @@ -123,13 +123,18 @@ async def integrity_error_handler( else: error_message = "Error de integridad en la base de datos" + content = { + "error": "DATABASE_INTEGRITY_ERROR", + "message": error_message, + "status_code": status.HTTP_409_CONFLICT, + } + + if settings.DEBUG: + content["debug_detail"] = str(exc.orig) + response = JSONResponse( status_code=status.HTTP_409_CONFLICT, - content={ - "error": "DATABASE_INTEGRITY_ERROR", - "message": error_message, - "status_code": status.HTTP_409_CONFLICT, - }, + content=content, ) for k, v in _cors_headers(request).items(): response.headers[k] = v diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index ca30b42e..688868a0 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -11,6 +11,7 @@ const API_BASE_URL = (import.meta.env.VITE_API_URL || '').replace(/\/+$/, ''); export interface ApiResponse { data?: T; error?: string; + details?: any; validationErrors?: Array<{ field: string; message: string; @@ -271,7 +272,8 @@ async function fetchApi( return { error: data.message || (typeof data.detail === 'string' ? data.detail : JSON.stringify(data.detail)) || 'Error en la petición', - status: response.status + status: response.status, + details: data }; } diff --git a/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte b/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte index 8f4bfe0d..06faf211 100644 --- a/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte +++ b/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte @@ -1,14 +1,11 @@
-
+
- + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} {#each headerGroup.headers as header (header.id)} @@ -103,22 +104,21 @@ {#each table.getRowModel().rows as row (row.id)} - { if (onRowClick) { onRowClick(row.original); } }} ondblclick={() => handleRowDoubleClick(row)} - class="cursor-pointer hover:bg-muted/50 transition-colors {row.getIsSelected() ? 'bg-primary/10' : ''}" + class="cursor-pointer transition-colors hover:bg-muted/50 {row.getIsSelected() + ? 'bg-primary/10' + : ''}" > {#each row.getVisibleCells() as cell (cell.id)} - + {/each} @@ -129,7 +129,7 @@ {/each} - + {#if hasMore} @@ -137,13 +137,13 @@
{#if loading}
-
- Cargando más... +
+ Cargando más...
{:else} -
- Desplázate para cargar más -
+
Desplázate para cargar más
{/if}
diff --git a/frontend/src/routes/dashboard/pedimentos/+page.svelte b/frontend/src/routes/dashboard/pedimentos/+page.svelte index 6898d88d..4ed9472d 100644 --- a/frontend/src/routes/dashboard/pedimentos/+page.svelte +++ b/frontend/src/routes/dashboard/pedimentos/+page.svelte @@ -79,30 +79,34 @@ let error = $state(data.error || null); // Estado para selección de filas - let selectedId = $state(null); - let hasSelection = $derived(selectedId !== null); + let selectedIds = $state([]); + let hasSelection = $derived(selectedIds.length > 0); let showDeleteDialog = $state(false); function handleRowClick(pedimento: Pedimento) { // Toggle: si ya está seleccionado, deseleccionar; si no, seleccionar - selectedId = selectedId === pedimento.id ? null : pedimento.id; + if (selectedIds.includes(pedimento.id)) { + selectedIds = selectedIds.filter((id) => id !== pedimento.id); + } else { + selectedIds = [...selectedIds, pedimento.id]; + } } function handleEditSelected() { - if (selectedId) { - window.location.href = `/dashboard/pedimentos/edit/${selectedId}`; + if (selectedIds.length === 1) { + goto(`/dashboard/pedimentos/edit/${selectedIds[0]}`); } } function handleDelete() { - if (!selectedId) { + if (selectedIds.length === 0) { return; } showDeleteDialog = true; } async function confirmDelete() { - if (!selectedId) return; + if (selectedIds.length === 0) return; const companyId = companyStore.activeCompany?.id; if (!companyId) { @@ -111,22 +115,26 @@ } try { - const response = await pedimentosApi.delete(selectedId, companyId); + // Eliminar pedimentos uno por uno (o podrías implementar un delete masivo en el API si existe) + // Basado en pedimentosApi.delete(id, companyId), lo haremos secuencialmente o en paralelo + const deletePromises = selectedIds.map((id) => pedimentosApi.delete(id, companyId)); + const results = await Promise.all(deletePromises); - if (response.error) { - console.error('🗑️ [Pedimentos] Error al eliminar:', response.error); - error = response.error; + const firstError = results.find((r) => r.error); + if (firstError) { + console.error('🗑️ [Pedimentos] Error al eliminar:', firstError.error); + error = firstError.error ?? null; return; } // Recargar datos await reloadData(); - selectedId = null; + selectedIds = []; showDeleteDialog = false; } catch (e) { console.error('🗑️ [Pedimentos] Error deleting:', e); - error = 'Error al eliminar el pedimento'; + error = 'Error al eliminar los pedimentos seleccionados'; } } @@ -174,7 +182,7 @@ window.location.reload(); }, 2000); } else { - error = response.error; + error = response.error ?? null; } return; } @@ -223,7 +231,7 @@ window.location.reload(); }, 2000); } else { - error = response.error; + error = response.error ?? null; } return; } @@ -281,7 +289,7 @@ window.location.reload(); }, 2000); } else { - error = response.error; + error = response.error ?? null; } return; } @@ -302,7 +310,7 @@ function handleCreateClick() { // Redirigir a la página de creación (reusa la página de edición con ID "new") - window.location.href = '/dashboard/pedimentos/edit/new'; + goto('/dashboard/pedimentos/edit/new'); } function handleSuccess() { @@ -430,7 +438,7 @@ {loading} {hasMore} {loadMore} - {selectedId} + {selectedIds} onRowClick={handleRowClick} /> @@ -443,17 +451,21 @@
- -
@@ -464,9 +476,19 @@ - ¿Eliminar pedimento? + + {#if selectedIds.length > 1} + ¿Eliminar {selectedIds.length} pedimentos? + {:else} + ¿Eliminar pedimento? + {/if} + - Esta acción no se puede deshacer. El pedimento será eliminado permanentemente. + Esta acción no se puede deshacer. {#if selectedIds.length > 1} + Los pedimentos seleccionados serán eliminados + {:else} + El pedimento será eliminado + {/if} permanentemente.
diff --git a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte index ccd4acc7..4d133757 100644 --- a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte @@ -503,9 +503,9 @@ if (hasTransportValue) { payload.pedimento_transport_means = { destination: generalFormData.pedimento_transport_means.destination || null, - entry_exit: generalFormData.pedimento_transport_means.entry_exit || null, - arrival: generalFormData.pedimento_transport_means.arrival || null, - departure: generalFormData.pedimento_transport_means.departure || null + entry_exit: generalFormData.pedimento_transport_means.entry_exit || '', + arrival: generalFormData.pedimento_transport_means.arrival || '', + departure: generalFormData.pedimento_transport_means.departure || '' }; } } @@ -945,16 +945,17 @@ companyStore.activeCompany?.id ); if (response.error) { + // Si hay un error detallado en la respuesta JSON, adjuntarlo + const errorData = + response.details || (typeof response.error === 'object' ? response.error : null); + console.error('Create error details:', errorData); + const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear el pedimento'; throw new Error(errorMsg); } if (!response.data?.id) throw new Error('No se recibió el ID del pedimento creado'); newPedimentoId = response.data.id; - - // Redirigir a la página de edición - await goto(`/dashboard/pedimentos/edit/${newPedimentoId}`); - return; } else { // Actualizar pedimento existente con todos sus sub-recursos const response = await pedimentosApi.update( @@ -962,17 +963,9 @@ cleanPayload as UpdatePedimentoData, companyStore.activeCompany?.id ); - if (response.error) throw new Error(response.error); - - // Recargar los datos del pedimento desde el servidor - try { - await invalidateAll(); - // Forzar recarga de datos esperando un tick - await new Promise((resolve) => setTimeout(resolve, 100)); - } catch (invalidateError) { - console.error('❌ Error en invalidateAll:', invalidateError); - // No lanzar el error, solo loguearlo - // El pedimento ya se guardó exitosamente en el backend + if (response.error) { + console.error('Update error details:', response.details || response.error); + throw new Error(response.error); } } @@ -981,7 +974,13 @@ ? 'Pedimento creado exitosamente' : 'Todos los cambios se guardaron correctamente' ); + + // Pequeña espera para que el usuario pueda ver el toast antes de redirigir + setTimeout(() => { + goto('/dashboard/pedimentos'); + }, 500); } catch (e) { + console.error('Detailed error saving all:', e); if (e instanceof Error) { if (e.message.includes('401')) { toast.error('Sesión expirada. Recargando página...'); @@ -1026,9 +1025,23 @@ // Si parece JSON, intentar formatearlo un poco o mostrar mensaje genérico try { const errObj = JSON.parse(errorStr); - // Si es del formato {"field": ["msg"]} - const values = Object.values(errObj).flat(); - displayError = values.join(', '); + + // Priorizar el campo 'message' o 'detail' que suelen enviar FastAPI/mis manejadores + if (errObj.message) { + displayError = errObj.message; + } else if (errObj.detail) { + if (Array.isArray(errObj.detail)) { + displayError = errObj.detail + .map((d: any) => d.msg || JSON.stringify(d)) + .join(', '); + } else { + displayError = String(errObj.detail); + } + } else { + // Si es del formato {"field": ["msg"]} + const values = Object.values(errObj).flat(); + displayError = values.join(', '); + } } catch { displayError = 'Error al guardar (ver consola)'; }