From 1130306fc75aade4da0975ee442dcc03ca3b0996 Mon Sep 17 00:00:00 2001 From: hreyes Date: Wed, 15 Apr 2026 15:58:52 -0600 Subject: [PATCH 1/8] feature/bug-no-scroll-input-classes --- backend/api/v1/modules/a76/classes/service.py | 28 +- frontend/src/lib/api/dashboard/a76/classes.ts | 4 +- .../goods/parts/class-selector-dialog.svelte | 346 +++++++++++------- .../edit/items/fa/class-dialog.svelte | 163 +++++---- 4 files changed, 328 insertions(+), 213 deletions(-) diff --git a/backend/api/v1/modules/a76/classes/service.py b/backend/api/v1/modules/a76/classes/service.py index 107faaf9..e62d619d 100644 --- a/backend/api/v1/modules/a76/classes/service.py +++ b/backend/api/v1/modules/a76/classes/service.py @@ -48,18 +48,30 @@ class ClassService: query = query.filter(Class.company_id == company_id) if filters: - if filters.get("class_code"): - query = query.filter( - Class.class_code.ilike(f"%{filters['class_code']}%") - ) - if filters.get("description"): - description_pattern = f"%{filters['description']}%" + # Búsqueda libre: OR en clave y descripciones (selectores / listados) + search_raw = filters.get("search") or filters.get("q") + if search_raw and str(search_raw).strip(): + pattern = f"%{str(search_raw).strip()}%" query = query.filter( or_( - Class.description_es.ilike(description_pattern), - Class.description_en.ilike(description_pattern), + Class.class_code.ilike(pattern), + Class.description_es.ilike(pattern), + Class.description_en.ilike(pattern), ) ) + else: + if filters.get("class_code"): + query = query.filter( + Class.class_code.ilike(f"%{filters['class_code']}%") + ) + if filters.get("description"): + description_pattern = f"%{filters['description']}%" + query = query.filter( + or_( + Class.description_es.ilike(description_pattern), + Class.description_en.ilike(description_pattern), + ) + ) if filters.get("material_key"): query = query.filter( Class.material_key.ilike(f"%{filters['material_key']}%") diff --git a/frontend/src/lib/api/dashboard/a76/classes.ts b/frontend/src/lib/api/dashboard/a76/classes.ts index 00c1cc4a..6f0eb86b 100644 --- a/frontend/src/lib/api/dashboard/a76/classes.ts +++ b/frontend/src/lib/api/dashboard/a76/classes.ts @@ -58,7 +58,9 @@ export interface A76ClassListParams { page_size?: number; class_code?: string; description?: string; - q?: string; // Agregado por si usas búsqueda general + /** Búsqueda OR en class_code, description_es y description_en (backend ClassService) */ + search?: string; + q?: string; sort_by?: string; sort_order?: 'asc' | 'desc'; } diff --git a/frontend/src/lib/components/dashboard/goods/parts/class-selector-dialog.svelte b/frontend/src/lib/components/dashboard/goods/parts/class-selector-dialog.svelte index 7a954a0a..cfa9e69c 100644 --- a/frontend/src/lib/components/dashboard/goods/parts/class-selector-dialog.svelte +++ b/frontend/src/lib/components/dashboard/goods/parts/class-selector-dialog.svelte @@ -1,156 +1,220 @@ - - - Seleccionar Clase (Anexo 24) - - Seleccione la clasificación del material. - - + + + Seleccionar Clase (Anexo 24) + + Seleccione la clasificación del material. Desplace hacia abajo para cargar más resultados. + + -
- - -
+
+ + +
-
- {#if loading} -
- -

Cargando catálogo...

-
- {:else if filteredItems.length === 0} -
-

No se encontraron clases.

-
- {:else} - - - - - - - - - - {#each filteredItems as item} - handleSelect(item)} - > - - - +
+ {#if loading} +
+ +

Cargando catálogo...

+
+ {:else if items.length === 0} +
+

No se encontraron clases.

+
+ {:else} +
ClaveDescripciónUM
- - {item.class_code} - - -
- - - {item.description_es || 'Sin descripción'} - -
-
+ + + + + + + + + {#each items as item} + handleSelect(item)} + > + - - - {/each} - -
ClaveDescripciónUM
+ + {item.class_code} + + -
- - {item.unit_of_measure || '-'} -
-
- {/if} -
+ +
+ + + {item.description_es || item.description_en || 'Sin descripción'} + +
+ - -
- {filteredItems.length} registros encontrados -
- -
-
-
\ No newline at end of file + +
+ + {item.unit_of_measure || '-'} +
+ + + {/each} + + + {#if loadingMore} +
+ +
+ {/if} + {/if} + + + +
+ {#if loading} + … + {:else} + Mostrando {items.length} de {total} registros + {/if} +
+ +
+ + diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/class-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/class-dialog.svelte index 1e74393d..05a4ad42 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/class-dialog.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/class-dialog.svelte @@ -6,6 +6,7 @@ import { Search, Loader2 } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; import { companyStore } from '$lib/stores/company.svelte'; + import { classesApi } from '$lib/api/dashboard/a76/classes'; let { open = $bindable(false), @@ -16,86 +17,109 @@ } = $props(); let searchQuery = $state(''); - let isSearching = $state(false); let classes = $state([]); - let displayedClasses = $state([]); + let total = $state(0); let currentPage = $state(1); - let itemsPerPage = 10; + let loading = $state(false); + let loadingMore = $state(false); - const filteredClasses = $derived( - searchQuery - ? classes.filter( - (c) => - c.class_code?.toLowerCase().includes(searchQuery.toLowerCase()) || - c.description_es?.toLowerCase().includes(searchQuery.toLowerCase()) - ) - : classes - ); + const PAGE_SIZE = 100; - $effect(() => { - if (open) { - searchClasses(); - } - }); + const hasMore = $derived(classes.length < total && total > 0); - $effect(() => { - currentPage = 1; - loadMoreClasses(); - }); - - async function searchClasses() { + async function loadPage(trimmed: string, page: number, append: boolean) { const activeCompanyId = companyStore?.activeCompany?.id; if (!activeCompanyId) { toast.error('No hay compañía activa'); return; } - isSearching = true; - try { - const response = await fetch( - `/api-sveltekit/classes?company_id=${activeCompanyId}&limit=100`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - } - ); + if (append) { + if (loadingMore || loading || !hasMore) return; + } else if (loading) { + return; + } - if (!response.ok) { - throw new Error('Error al buscar clases'); + if (page === 1) { + loading = true; + } else { + loadingMore = true; + } + + try { + const res = await classesApi.list({ + company_id: activeCompanyId, + page, + page_size: PAGE_SIZE, + ...(trimmed ? { search: trimmed } : {}) + }); + const data = (res as { data?: { items?: any[]; total?: number } }).data ?? res; + const raw = (data as { items?: any[] }).items ?? []; + const newItems = Array.isArray(raw) ? raw : []; + + if (append && newItems.length === 0) { + total = classes.length; + return; } - const data = await response.json(); - classes = data.items || []; - loadMoreClasses(); + const t = + typeof (data as { total?: number }).total === 'number' + ? (data as { total: number }).total + : append + ? classes.length + newItems.length + : newItems.length; + + if (append) { + classes = [...classes, ...newItems]; + } else { + classes = newItems; + } + total = t; + currentPage = page; } catch (error) { console.error('Error searching classes:', error); toast.error('Error al buscar clases'); - classes = []; + if (!append) { + classes = []; + total = 0; + } } finally { - isSearching = false; + loading = false; + loadingMore = false; } } - function loadMoreClasses() { - const start = 0; - const end = currentPage * itemsPerPage; - displayedClasses = filteredClasses.slice(start, end); - } - function handleScroll(e: Event) { - const target = e.target as HTMLDivElement; - const threshold = 100; - const scrolledToBottom = - target.scrollHeight - target.scrollTop - target.clientHeight < threshold; - - if (scrolledToBottom && displayedClasses.length < filteredClasses.length) { - currentPage++; - loadMoreClasses(); - } + const target = e.currentTarget as HTMLDivElement; + const threshold = 80; + if (target.scrollHeight - target.scrollTop - target.clientHeight > threshold) return; + if (!hasMore || loading || loadingMore) return; + const term = searchQuery.trim(); + loadPage(term, currentPage + 1, true); } + $effect(() => { + if (!open) { + searchQuery = ''; + classes = []; + total = 0; + currentPage = 1; + } + }); + + $effect(() => { + if (!open) return; + const activeCompanyId = companyStore?.activeCompany?.id; + if (!activeCompanyId) return; + + const term = searchQuery.trim(); + const delayMs = term === '' ? 0 : 300; + const handle = setTimeout(() => { + loadPage(term, 1, false); + }, delayMs); + return () => clearTimeout(handle); + }); + function handleSelect(classItem: any) { if (onSelect) { onSelect(classItem); @@ -108,7 +132,9 @@ Seleccionar Clase - Busca y selecciona una clase para la partida + + Busca y selecciona una clase para la partida. Desplázate hacia abajo para más resultados. +
@@ -122,8 +148,11 @@
-
- {#if isSearching} +
+ {#if loading}
@@ -138,14 +167,14 @@ - {#if displayedClasses.length === 0} + {#if classes.length === 0} No se encontraron clases {:else} - {#each displayedClasses as classItem} + {#each classes as classItem} handleSelect(classItem)} @@ -165,11 +194,19 @@ {/if} + {#if loadingMore} +
+ +
+ {/if} {/if}
- - + + {#if !loading && classes.length > 0} + Mostrando {classes.length} de {total} + {/if} + From c90776ec4f2f204f20dbe642ea38688fb3c8e827 Mon Sep 17 00:00:00 2001 From: hreyes Date: Thu, 16 Apr 2026 07:29:59 -0600 Subject: [PATCH 2/8] feature/fecha-pago-visible --- frontend/src/app.css | 8 ++++++++ frontend/src/lib/components/ui/input/input.svelte | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/app.css b/frontend/src/app.css index 627e3836..af7ea21c 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -120,6 +120,14 @@ body { @apply bg-background text-foreground overflow-x-hidden; } + + /* Asegurar que el texto de los inputs de fecha sea legible en modo oscuro + y en navegadores WebKit */ + input[type="date"], + input[type="datetime-local"] { + color: var(--color-foreground); + -webkit-text-fill-color: var(--color-foreground); + } } @layer components { diff --git a/frontend/src/lib/components/ui/input/input.svelte b/frontend/src/lib/components/ui/input/input.svelte index 960167d7..ef1fbe7d 100644 --- a/frontend/src/lib/components/ui/input/input.svelte +++ b/frontend/src/lib/components/ui/input/input.svelte @@ -25,7 +25,7 @@ bind:this={ref} data-slot={dataSlot} class={cn( - "selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50", + "selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium text-foreground outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className @@ -40,7 +40,7 @@ bind:this={ref} data-slot={dataSlot} class={cn( - "border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base text-foreground outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className From cf7d5c31cf3f93a1444249ea66a63d5ac7e74dc9 Mon Sep 17 00:00:00 2001 From: hreyes Date: Thu, 16 Apr 2026 08:06:51 -0600 Subject: [PATCH 3/8] fix/convertion-icon-clear-data --- .../goods/modales/unit-measure-dialog.svelte | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/components/dashboard/goods/modales/unit-measure-dialog.svelte b/frontend/src/lib/components/dashboard/goods/modales/unit-measure-dialog.svelte index ff8b057e..81ee5af0 100644 --- a/frontend/src/lib/components/dashboard/goods/modales/unit-measure-dialog.svelte +++ b/frontend/src/lib/components/dashboard/goods/modales/unit-measure-dialog.svelte @@ -19,6 +19,7 @@ let allItems = $state([]); // Store full dataset let loading = $state(false); let searchTerm = $state(""); + let wasOpen = $state(false); // Derived state for filtering $effect(() => { @@ -34,11 +35,15 @@ } }); - // Cargar datos al abrir + // Cargar datos al abrir y resetear búsqueda cada vez que se abre $effect(() => { - if (open && companyStore.activeCompany && allItems.length === 0) { - loadData(); + if (open && !wasOpen) { + searchTerm = ""; + if (companyStore.activeCompany && allItems.length === 0) { + loadData(); + } } + wasOpen = open; }); async function loadData() { From 991dd924eeb72b2a18c9c7423aff1852a2a5ddf0 Mon Sep 17 00:00:00 2001 From: hreyes Date: Thu, 16 Apr 2026 08:36:41 -0600 Subject: [PATCH 4/8] feature/doble-clic-invoices --- .../lib/components/dashboard/invoices/data-table.svelte | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/lib/components/dashboard/invoices/data-table.svelte b/frontend/src/lib/components/dashboard/invoices/data-table.svelte index fc431493..ae206e98 100644 --- a/frontend/src/lib/components/dashboard/invoices/data-table.svelte +++ b/frontend/src/lib/components/dashboard/invoices/data-table.svelte @@ -70,6 +70,13 @@ manualSorting: true // Sorting is handled server-side for this component }); + function handleRowDoubleClick(row: any) { + const invoice = row.original; + if (invoice?.id) { + window.location.href = `/dashboard/invoices/edit/${invoice.id}`; + } + } + let scrollContainer = $state(); let loadingTrigger = $state(); @@ -200,6 +207,7 @@ data-state={row.getIsSelected() && 'selected'} class="group/inv-list cursor-pointer {row.getIsSelected() ? 'catalog-table-row-selected' : 'catalog-table-row'}" onclick={() => onRowClick && onRowClick(row.original)} + ondblclick={() => handleRowDoubleClick(row)} > {#each visibleCells as cell (cell.id)} {@const colId = cell.column.id} From e83be520bbcc7746fb49c867ca0c4721ad7ffd48 Mon Sep 17 00:00:00 2001 From: hreyes Date: Thu, 16 Apr 2026 08:53:13 -0600 Subject: [PATCH 5/8] feature/atajos-nav-invoices --- .../dashboard/pedimentos/data-table.svelte | 9 ++-- .../shortcuts/dashboard/invoices/list.ts | 2 +- .../shortcuts/dashboard/pedimentos/list.ts | 14 +++++++ .../routes/dashboard/pedimentos/+page.svelte | 42 ++++++++++++++++++- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte b/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte index eb51c5d2..e6c1e89c 100644 --- a/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte +++ b/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte @@ -96,7 +96,7 @@ }); -
+
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} {@const colId = header.column.id} {#each table.getRowModel().rows as row (row.id)} { if (onRowClick) { @@ -208,7 +209,7 @@ {/each} {:else} - + No hay resultados. @@ -217,7 +218,7 @@ {#if hasMore} - +
{#if loading} diff --git a/frontend/src/lib/config/shortcuts/dashboard/invoices/list.ts b/frontend/src/lib/config/shortcuts/dashboard/invoices/list.ts index 85d88179..6db05b7c 100644 --- a/frontend/src/lib/config/shortcuts/dashboard/invoices/list.ts +++ b/frontend/src/lib/config/shortcuts/dashboard/invoices/list.ts @@ -27,7 +27,7 @@ export const obtenerAtajosListaFacturas = (acciones: { }): ShortcutDef[] => [ { key: 'Alt+V', description: 'Ver Temporales', action: acciones.irTemporal }, { key: 'Alt+F', description: 'Ver Definitivas', action: acciones.irDefinitiva }, - { key: 'Alt+N', description: 'Ver Nacionales', action: acciones.irNacional }, + { key: 'Alt+Q', description: 'Ver Nacionales', action: acciones.irNacional }, { key: 'Alt+Z', description: 'Ver Cambio Régimen', action: acciones.irCambioRegimen }, { key: 'Alt+L', description: 'Ver Exportaciones', action: acciones.irExportacion }, { key: 'Alt+Y', description: 'Ver Reparaciones', action: acciones.irReparacion }, diff --git a/frontend/src/lib/config/shortcuts/dashboard/pedimentos/list.ts b/frontend/src/lib/config/shortcuts/dashboard/pedimentos/list.ts index 84fe8028..9c635bd3 100644 --- a/frontend/src/lib/config/shortcuts/dashboard/pedimentos/list.ts +++ b/frontend/src/lib/config/shortcuts/dashboard/pedimentos/list.ts @@ -5,6 +5,8 @@ export const obtenerAtajosListaPedimento = (acciones: { manejarActualizar: () => void; manejarEditar: () => void; manejarEliminar: () => void; + irATabla: () => void; + irAAcciones: () => void; }): ShortcutDef[] => [ { key: 'Alt+Shift+N', @@ -25,5 +27,17 @@ export const obtenerAtajosListaPedimento = (acciones: { key: 'Alt+Shift+D', description: 'Eliminar Seleccionado', action: acciones.manejarEliminar + }, + { + key: 'Alt+Shift+T', + description: 'Ir a tabla de pedimentos', + action: acciones.irATabla, + skipDefaultFocusAfter: true + }, + { + key: 'Alt+Shift+A', + description: 'Ir a acciones (barra inferior)', + action: acciones.irAAcciones, + skipDefaultFocusAfter: true } ]; diff --git a/frontend/src/routes/dashboard/pedimentos/+page.svelte b/frontend/src/routes/dashboard/pedimentos/+page.svelte index 0fedc396..a6c421d9 100644 --- a/frontend/src/routes/dashboard/pedimentos/+page.svelte +++ b/frontend/src/routes/dashboard/pedimentos/+page.svelte @@ -230,13 +230,52 @@ let filters = $state({ } // Keyboard Shortcuts + /** Teclado: foco en la primera fila de datos de la tabla */ + function focusFirstPedimentoTableRow() { + if (!browser) return; + const row = document.querySelector( + '[data-pedimento-list-table] tbody tr[data-pedimento-data-row][tabindex="0"]' + ); + if (row) { + row.focus(); + setTimeout(() => row.scrollIntoView({ behavior: 'smooth', block: 'nearest' }), 50); + return; + } + toast.info('No hay filas en la tabla'); + } + + /** Teclado: foco en el primer control habilitado de la barra de acciones fija */ + function focusPedimentoListFooterActions() { + if (!browser) return; + const footer = document.getElementById('pedimento-list-footer'); + if (!footer) return; + const candidates = footer.querySelectorAll( + 'button, a[href], input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])' + ); + for (const el of candidates) { + if (el.offsetParent === null && el.getClientRects().length === 0) continue; + if (el.hasAttribute('disabled')) continue; + if ((el as HTMLButtonElement).disabled) continue; + el.focus(); + setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }), 50); + return; + } + try { + footer.focus(); + } catch { + /* ignore */ + } + } + useShortcuts( 'Pedimentos', obtenerAtajosListaPedimento({ manejarCrear: handleCreateClick, manejarActualizar: reloadData, manejarEditar: handleEditSelected, - manejarEliminar: handleDelete + manejarEliminar: handleDelete, + irATabla: focusFirstPedimentoTableRow, + irAAcciones: focusPedimentoListFooterActions }) ); @@ -535,6 +574,7 @@ let filters = $state({ @@ -550,6 +592,7 @@ bind:lineItem={editingItem} bind:descriptions={editingItem.description} visibility={visibility} + disabled={isReadOnly} /> {/if} @@ -561,13 +604,19 @@ bind:series={editingItem.series} lineItem={editingItem} {invoice} + disabled={isReadOnly} /> {/if} {#if visibility.showLabelingTab} - + {/if} @@ -578,6 +627,7 @@ invoiceConsecutive={invoice?.id} invoiceNumber={invoice?.invoice_number ?? ''} {visibility} + disabled={isReadOnly} /> {/if} @@ -595,17 +645,20 @@
- + + {#if !isReadOnly} + + {/if}
@@ -613,10 +666,10 @@ {/each}
- - + + - - - - - Seleccionar línea de importación -

- Solo se muestran líneas con saldo disponible -

+ + + + +
+
+ +
+ Partidas de Importación +
+ + Selecciona una línea con saldo disponible para realizar la descarga. +
- -
-
- {#if importInvoiceLines.every(l => !l.has_balance)} -

- No hay líneas con saldo disponible en esta factura. -

+ +
+ {#if loadingImportLines} +
+ +

Cargando partidas de la factura...

+
+ {:else if importInvoiceLines.every(l => !l.has_balance)} +
+ +

Sin saldo disponible

+

No hay líneas con saldo en esta factura para descargar.

+
{:else} - - - - - - - - - - - - - - - - - - - - {#each importInvoiceLines as lineItem} - {#if lineItem.has_balance} - { +
+ {#each importInvoiceLines as lineItem} + {#if lineItem.has_balance} +
- - - - - - - - - - - - - {/if} - {/each} - -
LíneaFacturaFechaNum. ParteClaseDescripciónCant. Imp.Ret. Temp.Ret. Def.Saldo Disp.EstatusSub.
{lineItem.line_number}{lineItem.invoice_number ?? '-'} - {lineItem.invoice_date ? lineItem.invoice_date.slice(0, 10) : '-'} - {lineItem.part_number ?? '-'}{lineItem.class_code ?? '-'} - {lineItem.description_spanish ?? '-'} - - {lineItem.quantity != null ? lineItem.quantity.toLocaleString('es-MX', { maximumFractionDigits: 4 }) : '-'} - {lineItem.unit_of_measure_code ?? ''} - - {lineItem.quantity_used_temp != null ? lineItem.quantity_used_temp.toLocaleString('es-MX', { maximumFractionDigits: 4 }) : '-'} - - {lineItem.quantity_used_def != null ? lineItem.quantity_used_def.toLocaleString('es-MX', { maximumFractionDigits: 4 }) : '-'} - - {lineItem.available_balance.toLocaleString('es-MX', { maximumFractionDigits: 4 })} - {lineItem.unit_of_measure_code ?? ''} - + } catch (err) { + console.error('Error auto-filling import info:', err); + } finally { + loadingImportDetails = false; + } + }} + > + +
+
+
+ Línea {lineItem.line_number} +
+
+ Factura: {lineItem.invoice_number ?? '-'} +
+
+
{#if lineItem.invoice_status === 'processed'} - + Procesada - {:else if lineItem.invoice_status === 'reversed'} - - Revertida - - {:else} - - {lineItem.invoice_status ?? 'Pendiente'} - {/if} -
{#if lineItem.is_subitem} - - Sub + + Subpartida - {:else if lineItem.contains_subitems} - - {lineItem.subitem_count ?? 0} sub - - {:else} - {/if} -
+
+
+ + +
+ +
+

Número de Parte / Clase

+

+ {lineItem.part_number ?? '-'} +

+

+ {lineItem.class_code ?? 'Sin Clase'} +

+
+ + +
+

Descripción

+

+ {lineItem.description_spanish || 'Sin descripción'} +

+
+ + +
+
+ Cant. Imp: + + {lineItem.quantity?.toLocaleString() || '0'} {lineItem.unit_of_measure_code || ''} + +
+
+ Desc. Temp: + + -{lineItem.quantity_used_temp?.toLocaleString() || '0'} + +
+
+ + +
+

Saldo Disponible

+
+ + {lineItem.available_balance.toLocaleString('es-MX', { maximumFractionDigits: 4 })} + + + {lineItem.unit_of_measure_code ?? ''} + +
+
+
+ +
+
+ + Fecha: {lineItem.invoice_date ? lineItem.invoice_date.slice(0, 10) : 'N/A'} +
+
+ Hacer descarga → +
+
+ + {/if} + {/each} +
{/if} -
+ + +

+ Mostrando {importInvoiceLines.filter(l => l.has_balance).length} partidas con saldo +

+ +
\ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte index f279e7cb..04e6fcaf 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte @@ -16,15 +16,20 @@ quantities = $bindable(), financials = $bindable(), customs = $bindable(), - invoice + invoice, + disabled = false }: { lineItem: Partial; quantities: LineQuantities; financials: LineFinancials; customs: LineCustoms; invoice: Invoice | null; + disabled?: boolean; } = $props(); + + const activeCompanyId = $derived(companyStore?.activeCompany?.id); + let showClassDialog = $state(false); let showUnitDialog = $state(false); let showCountryDialog = $state(false); @@ -47,6 +52,8 @@ return frac; }); + let isDischargeActive = $derived(lineItem.fa_data?.discharge === true); + // Track previous class_id to detect changes let previousClassId = $state(undefined); @@ -132,8 +139,17 @@ $effect(() => { const currentClassId = lineItem.class_id; const activeCompanyId = companyStore?.activeCompany?.id; + + // Initial load protection: If previousClassId is undefined, this is the first run. + // We set previousClassId to the current value without fetching catalog defaults + // if we are opening an existing record that already has a class. + if (previousClassId === undefined && currentClassId) { + previousClassId = currentClassId; + return; + } - // Only fetch if class_id changed, is valid, and we have a company + // Only fetch and apply defaults if class_id changed from a previous value, + // is valid, and we have a company. if (currentClassId && currentClassId !== previousClassId && activeCompanyId) { previousClassId = currentClassId; @@ -188,40 +204,124 @@ // Auto-fetch historical tariff rate when fraction, country, type, and date are available $effect(() => { const fraction = customs.fraction?.replace(/\./g, '') || ''; - const nico = fraction.substring(8, 10); - const fractionType = customs.fraction_type; + const nico = fraction.length >= 10 ? fraction.substring(8, 10) : '00'; + const tariffType = customs.fraction_type || 'GENERAL'; const invoiceDate = invoice?.invoice_date; + + // Map invoice movement type to direction string + const direction = invoice?.operation_type === 'imp' ? 'import' : 'export'; // Only fetch if all required fields are present and fraction has at least 8 chars - if (fraction && fraction.length >= 8 && nico && fractionType && invoiceDate) { + if (activeCompanyId && fraction && fraction.length >= 8 && tariffType && invoiceDate) { const historicalFraction = fraction.substring(0, 8); + const isRegimeChange = invoice?.compliance_mx?.is_regime_change ? 'true' : 'false'; + + // Format date to YYYY-MM-DD + let formattedDate = ''; + if (invoiceDate) { + const dateObj = typeof invoiceDate === 'string' ? new Date(invoiceDate) : invoiceDate; + formattedDate = dateObj.toISOString().split('T')[0]; + } + + if (!formattedDate) return; + const params = new URLSearchParams({ + company_id: activeCompanyId.toString(), historical_fraction: historicalFraction, nico: nico, - fraction_type: fractionType, - invoice_date: invoiceDate + direction: direction, + tariff_type: tariffType, + invoice_date: formattedDate, + is_regime_change: isRegimeChange }); fetch(`/api-sveltekit/historical-tariff-fractions/rate?${params}`) - .then(response => { + .then(async response => { if (response.ok) { return response.json(); } - throw new Error('Failed to fetch tariff rate'); + // If 422 or other error, try to extract the specific detail from FastAPI + let errorMsg = `HTTP ${response.status}`; + try { + const errData = await response.json(); + if (errData.detail) { + errorMsg = typeof errData.detail === 'string' ? errData.detail : JSON.stringify(errData.detail); + } else if (errData.details || errData.error) { + errorMsg = errData.details || errData.error; + } + } catch (e) { + errorMsg = await response.text().catch(() => `Error ${response.status}`); + } + + console.warn('Historical tariff lookup failed at backend:', errorMsg); + return { found: false, rate: 0 }; }) .then(data => { - if (data.found && data.rate !== null) { - customs.rate = data.rate; + if (data && data.found && data.rate !== null) { + customs.rate = String(data.rate).substring(0, 10); } else { customs.rate = '0'; } }) .catch(error => { - console.error('Error fetching historical tariff rate:', error); - // Keep current value on error + console.warn('Historical tariff lookup network error:', error); }); } }); + + /** + * Centralized calculation logic based on the Clarion routine + * @param source - Which field triggered the update + */ + function recalculateFinancials(source: 'total' | 'unit_cost' | 'quantity') { + const qty = Number(quantities.quantity || 0); + const exchangeRate = Number(invoice?.financials?.exchange_rate || 1); + const exchangeRateMM = Number(invoice?.financials?.exchange_rate_mm || 1); + const ivaFactor = Number(invoice?.financials?.iva_factor || 0); + const currencyType = invoice?.financials?.currency_type || 'USD'; + + // 1. Synchronize Unit Cost and Total + if (source === 'total') { + if (qty > 0) { + financials.unit_cost_capture = Number(financials.value_mc || 0) / qty; + } + } else { + // source is unit_cost or quantity + financials.value_mc = Number(financials.unit_cost_capture || 0) * qty; + } + + const unitCostCapture = Number(financials.unit_cost_capture || 0); + const valueCapture = Number(financials.value_mc || 0); + + // 2. Perform Triangulation based on Currency Type + if (currencyType === 'ME' || currencyType === 'FOREIGN' || currencyType === 'USD') { + financials.unit_cost_usd = unitCostCapture; + financials.unit_cost_mxn = unitCostCapture * exchangeRate; + financials.value_usd = valueCapture; + financials.value_mxn = valueCapture * exchangeRate; + } else if (currencyType === 'MN' || currencyType === 'LOCAL' || currencyType === 'MXN') { + financials.unit_cost_mxn = unitCostCapture; + financials.unit_cost_usd = exchangeRate > 0 ? unitCostCapture / exchangeRate : 0; + financials.value_mxn = valueCapture; + financials.value_usd = exchangeRate > 0 ? valueCapture / exchangeRate : 0; + } else if (currencyType === 'MC') { + financials.unit_cost_mc = unitCostCapture; + // Clarion logic for MC: + // 1. Convert Capture to USD using exchangeRateMM (TipoCambioMM) + financials.unit_cost_usd = unitCostCapture * exchangeRateMM; + // 2. Convert resulting USD to MXN using exchangeRate (TipoCambio) + financials.unit_cost_mxn = financials.unit_cost_usd * exchangeRate; + + financials.value_mc = valueCapture; + financials.value_usd = financials.unit_cost_usd * qty; + financials.value_mxn = financials.unit_cost_mxn * qty; + } + + // 3. VAT Calculation + financials.vat_mxn = (Number(financials.value_mxn || 0) * ivaFactor) / 100; + financials.vat_usd = (Number(financials.value_usd || 0) * ivaFactor) / 100; + financials.vat_mc = (Number(financials.value_mc || 0) * ivaFactor) / 100; + } @@ -242,16 +342,19 @@ type="text" value={(lineItem as any).class_code || ''} readonly + disabled={disabled || isDischargeActive} class="h-8 text-xs flex-1 bg-muted cursor-pointer" placeholder="Selecciona una clase" - onclick={() => (showClassDialog = true)} + onclick={() => !disabled && !isDischargeActive && (showClassDialog = true)} />
@@ -263,7 +366,22 @@
- + recalculateFinancials('quantity')} + class="h-8 text-xs text-right" + /> + + {#if isDischargeActive && lineItem.fa_data?.source_balance !== undefined && Number(quantities.quantity) > Number(lineItem.fa_data.source_balance)} +

+ ⚠️ Excede saldo disponible ({lineItem.fa_data.source_balance}) +

+ {/if}
@@ -274,18 +392,21 @@ type="text" value={(lineItem as any).unit_code || ''} readonly + disabled={disabled || isDischargeActive} class="h-8 text-xs flex-1 bg-muted cursor-pointer" placeholder="Selecciona U.M." - onclick={() => (showUnitDialog = true)} + onclick={() => !disabled && !isDischargeActive && (showUnitDialog = true)} /> +
@@ -293,8 +414,36 @@
- - USD + recalculateFinancials('unit_cost')} + class="h-8 text-xs text-right flex-1" + /> + + {invoice?.financials?.currency_type || 'USD'} +
+
+ +
+ +
+ recalculateFinancials('total')} + class="h-8 text-xs text-right flex-1" + /> + + {invoice?.financials?.currency_type || 'USD'}
@@ -305,18 +454,21 @@ id="fraccion" value={fractionDisplay()} readonly + disabled={disabled} class="h-8 text-xs text-center flex-1 bg-muted cursor-pointer" placeholder="Selecciona fracción" - onclick={() => (showFractionDialog = true)} + onclick={() => !disabled && (showFractionDialog = true)} /> + @@ -328,30 +480,34 @@ id="pais_origen" value={customs.origin_country || ''} readonly + disabled={disabled} class="h-8 text-xs flex-1 bg-muted cursor-pointer" placeholder="Selecciona país" - onclick={() => (showCountryDialog = true)} + onclick={() => !disabled && (showCountryDialog = true)} /> +
- +
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 61c68f09..32d494c3 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 @@ -3,7 +3,8 @@ import { Label } from '$lib/components/ui/label'; import { Button } from '$lib/components/ui/button'; import { Folder } from 'lucide-svelte'; - import type { Item, LineItem, LineDescriptions, LineCustoms, LineQuantities } from '$lib/api/dashboard/a76/items'; + import type { Item, LineDescriptions, LineCustoms, LineQuantities } from '$lib/api/dashboard/a76/items'; + import type { Invoice } from '$lib/api/dashboard/a76/invoices'; import PackageDialog from './package-dialog.svelte'; @@ -13,16 +14,20 @@ descriptions = $bindable(), customs = $bindable(), quantities = $bindable(), - invoice + invoice, + disabled = false }: { item: Partial; - lineItem: LineItem; + lineItem: any; descriptions: LineDescriptions; customs: LineCustoms; quantities: LineQuantities; invoice: Invoice | null; + disabled?: boolean; } = $props(); + + let packageDialogOpen = $state(false); let package_key = $state(''); let package_weight_unit = $state(0); @@ -117,8 +122,9 @@
- +
+
@@ -127,17 +133,22 @@ bind:value={package_key} class="h-7 text-xs flex-1" readonly + disabled={disabled} placeholder="Seleccionar..." + onclick={() => !disabled && (packageDialogOpen = true)} /> + +
@@ -159,12 +170,14 @@
- +
+
- +
+
{weightUnitLabel} @@ -175,19 +188,22 @@
- +
+
- +
+
- +
+
Advalorem: {customs.advalorem_american || '0.00'} @@ -197,16 +213,19 @@
- +
+
- +
+
- +
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/part-number-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/part-number-dialog.svelte index bcb53791..c60d96a6 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/part-number-dialog.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/part-number-dialog.svelte @@ -3,7 +3,7 @@ import * as Table from '$lib/components/ui/table'; import { Input } from '$lib/components/ui/input'; import { Button } from '$lib/components/ui/button'; - import { Search, Loader2 } from 'lucide-svelte'; + import { Search, Loader2, Info, Package } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; import { companyStore } from '$lib/stores/company.svelte'; @@ -16,160 +16,227 @@ } = $props(); let searchQuery = $state(''); + let debouncedSearch = $state(''); let isSearching = $state(false); + let isLoadingMore = $state(false); let parts = $state([]); - let displayedParts = $state([]); let currentPage = $state(1); - let itemsPerPage = 10; + let hasMore = $state(true); + let totalItems = $state(0); + const itemsPerPage = 25; - const filteredParts = $derived( - searchQuery - ? parts.filter(p => - p.part_number?.toLowerCase().includes(searchQuery.toLowerCase()) || - p.description_spanish?.toLowerCase().includes(searchQuery.toLowerCase()) || - p.description_english?.toLowerCase().includes(searchQuery.toLowerCase()) - ) - : parts - ); - - $effect(() => { - if (open) { - searchParts(); - } - }); - - $effect(() => { - currentPage = 1; - loadMoreParts(); - }); - - async function searchParts() { + async function fetchParts(page: number = 1, search: string = '') { const activeCompanyId = companyStore?.activeCompany?.id; - if (!activeCompanyId) { - toast.error('No hay compañía activa'); - return; - } + if (!activeCompanyId) return; + + if (page === 1) isSearching = true; + else isLoadingMore = true; - isSearching = true; try { - const response = await fetch( - `/api-sveltekit/parts?company_id=${activeCompanyId}&limit=100`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - } - ); + const params = new URLSearchParams({ + company_id: activeCompanyId.toString(), + page: page.toString(), + page_size: itemsPerPage.toString(), + sort_by: 'part_number', + sort_order: 'asc' + }); - if (!response.ok) { - throw new Error('Error al buscar números de parte'); + if (search) { + params.append('q', search); } + const response = await fetch(`/api-sveltekit/parts?${params.toString()}`); + if (!response.ok) throw new Error('Error al buscar números de parte'); + const data = await response.json(); - parts = data.items || []; - loadMoreParts(); + const newItems = data.items || []; + + if (page === 1) { + parts = newItems; + } else { + parts = [...parts, ...newItems]; + } + + totalItems = data.total || 0; + hasMore = newItems.length === itemsPerPage; + currentPage = page; } catch (error) { - console.error('Error searching parts:', error); - toast.error('Error al buscar números de parte'); - parts = []; + console.error('Error fetching parts:', error); + toast.error('Error al cargar números de parte'); } finally { isSearching = false; + isLoadingMore = false; } } - function loadMoreParts() { - const start = 0; - const end = currentPage * itemsPerPage; - displayedParts = filteredParts.slice(start, end); - } - - function handleScroll(e: Event) { - const target = e.target as HTMLDivElement; - const threshold = 100; - const scrolledToBottom = target.scrollHeight - target.scrollTop - target.clientHeight < threshold; + // Debounce effect + $effect(() => { + // Accedemos a searchQuery para que el efecto dependa de él + const query = searchQuery; - if (scrolledToBottom && displayedParts.length < filteredParts.length) { - currentPage++; - loadMoreParts(); - } - } + const timeout = setTimeout(() => { + if (debouncedSearch !== query) { + debouncedSearch = query; + currentPage = 1; + fetchParts(1, query); + } + }, 400); + + return () => clearTimeout(timeout); + }); function handleSelect(part: any) { - if (onSelect) { - onSelect(part); - } + if (onSelect) onSelect(part); open = false; } + + // Intersection Observer for Infinite Scroll + let observerNode: HTMLElement | null = $state(null); + + $effect(() => { + if (!observerNode || !hasMore || isSearching || isLoadingMore) return; + + const observer = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) { + fetchParts(currentPage + 1, debouncedSearch); + } + }, { threshold: 0.1 }); + + observer.observe(observerNode); + return () => observer.disconnect(); + }); + + // Reset state when opening + $effect(() => { + if (open) { + currentPage = 1; + searchQuery = ''; + debouncedSearch = ''; + fetchParts(1, ''); + } + }); - - - Seleccionar Número de Parte - - Busca y selecciona un número de parte para la partida + + +
+
+ +
+ Números de Parte +
+ + Busca y selecciona un número de parte del inventario maestro.
-
-
- +
+
+ + {#if isSearching} +
+ +
+ {/if}
-
- {#if isSearching} -
- -
- {:else} +
+
- - - Número de Parte - Descripción (ES) - Descripción (EN) - Clase - + + + Número de Parte + Descripción (ES) + Descripción (EN) + Clase + - {#if displayedParts.length === 0} - - - No se encontraron números de parte + {#each parts as part (part.id)} + handleSelect(part)} + > + + {part.part_number} + + +
+ {part.description_spanish || '-'} +
+
+ +
+ {part.description_english || '-'} +
+
+ + + {part.part_class || '-'} + + + +
+ +
{:else} - {#each displayedParts as part} - handleSelect(part)}> - {part.part_number} - {part.description_spanish || '-'} - {part.description_english || '-'} - {part.part_class || '-'} - - + {#if !isSearching} + + +
+
+ +
+

No hay resultados para esta búsqueda

+

Verifica el número de parte o la descripción

+
- {/each} + {/if} + {/each} + + + {#if hasMore} + + +
+ {#if isLoadingMore} +
+ + Cargando más números de parte... +
+ {/if} +
+
+
{/if}
- {/if} +
- -

- Mostrando {displayedParts.length} de {filteredParts.length} resultados -

+ +
+ + Mostrando {parts.length} de {totalItems} registros +
+
+ +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte index 60317a0c..dc0479bf 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte @@ -8,14 +8,17 @@ financials = $bindable(), quantities = $bindable(), lineItem, - invoice + invoice, + disabled = false }: { financials: LineFinancials; quantities: LineQuantities; lineItem?: Partial; invoice?: Invoice | null; + disabled?: boolean; } = $props(); + // Helper function to safely format numbers function formatNumber(value: any, decimals: number = 8): string { const num = Number(value); diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte index d071bc47..b48c1ab8 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte @@ -13,13 +13,16 @@ let { lineItem = $bindable(), descriptions = $bindable(), - visibility + visibility, + disabled = false }: { lineItem: Partial; descriptions: LineDescriptions; visibility: InvoiceItemVisibility; + disabled?: boolean; } = $props(); + let taxPaidValue = $derived(lineItem.tax_payment ? 'si' : 'no'); function setTaxPaid(val: string) { lineItem.tax_payment = val === 'si'; @@ -108,7 +111,9 @@ +
@@ -123,17 +128,19 @@
- +
+
@@ -145,12 +152,13 @@
- +
- +
+
{/if} @@ -159,17 +167,18 @@
FDA / FCC
- +
- +
- +
+
{/if} @@ -182,7 +191,9 @@ +
@@ -195,10 +206,11 @@
- + - +
+
{/if} @@ -209,22 +221,24 @@
- +
- +
+
{/if} @@ -238,11 +252,13 @@ +
@@ -257,21 +273,23 @@ {#if visibility.showContinuationMilitary}
- +
+ {/if} - {#if visibility.showContinuationOwnOmitAnnex} + {#if visibility.showContinuationOwnOmitAnnex && lineItem.fa_data}
- +
- +
+
{/if} @@ -280,12 +298,13 @@
- +
- +
+
{/if}
@@ -298,28 +317,30 @@
- +
- +
- +
+
{/if} {#if visibility.showContinuationConsiderA31}
- +
+ {/if} {#if visibility.showContinuationExtraDescription} @@ -328,10 +349,12 @@
+ {/if}
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte index 9600aa83..0f88ad96 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte @@ -14,14 +14,17 @@ lineItem = $bindable(), invoiceConsecutive = undefined, invoiceNumber = '', - visibility = { showMexicanIdEnhanced: false } + visibility = { showMexicanIdEnhanced: false }, + disabled = false }: { lineItem: Partial, invoiceConsecutive?: number, invoiceNumber?: string, - visibility?: any + visibility?: any, + disabled?: boolean } = $props(); + // Initialize identifiers if not present if (!lineItem.identifiers) { lineItem.identifiers = []; @@ -148,12 +151,13 @@
-
+
@@ -162,9 +166,12 @@ Num. Factura Línea Imagen - Acciones + {#if !disabled} + Acciones + {/if} + {#if lineItem.series && lineItem.series.length > 0} {#each lineItem.series as asset, index} @@ -181,17 +188,21 @@ - {/if} - -
- - -
-
+ {#if !disabled} + + +
+ + +
+
+ {/if} + {/each} {:else} @@ -212,12 +223,13 @@
-
+
@@ -226,9 +238,12 @@ Compl. 1 Compl. 2 Compl. 3 - Acciones + {#if !disabled} + Acciones + {/if} + {#if lineItem.identifiers && lineItem.identifiers.length > 0} {#each lineItem.identifiers as idDetail, index} @@ -237,17 +252,20 @@ {idDetail.complement1 || '-'} {idDetail.complement2 || '-'} {idDetail.complement3 || '-'} - -
- - -
-
+ {#if !disabled} + +
+ + +
+
+ {/if} + {/each} {:else} diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte index bc9d3eb9..2e475913 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte @@ -10,13 +10,16 @@ let { lineItem = $bindable(), descriptions = $bindable(), - visibility + visibility, + disabled = false }: { lineItem: Partial, descriptions: LineDescriptions, - visibility: any + visibility: any, + disabled?: boolean } = $props(); + // Ensure series is an array if (!lineItem.series) { lineItem.series = []; @@ -90,12 +93,13 @@
- +
- +
+
{/if} @@ -108,9 +112,11 @@ id="cantidad_importar" type="number" bind:value={lineItem.quantity!.quantity} + disabled={disabled} class="h-7 text-xs" />
+ {/if} {#if visibility.showLabelingValuationValue}
@@ -120,9 +126,11 @@ type="number" step="0.00000001" bind:value={lineItem.valuation_determined_value} + disabled={disabled} class="h-7 text-xs" />
+ {/if} {/if}
@@ -135,6 +143,7 @@ @@ -142,12 +151,14 @@ size="icon" variant="outline" class="h-7 w-7" - onclick={() => valuationSelectorOpen = true} + disabled={disabled} + onclick={() => (valuationSelectorOpen = true)} >
+ {/if} {#if visibility.showUsageReason} @@ -156,10 +167,12 @@ + {/if} {/if} @@ -170,10 +183,12 @@ + {/if} {/if} @@ -184,11 +199,12 @@ Assets / Series
-
+
@@ -197,9 +213,12 @@ Asset Num Factura Línea - Acc + {#if !disabled} + Acc + {/if} + {#each lineItem.series || [] as asset, i} @@ -207,17 +226,20 @@ {asset.number_id || '-'} {asset.import_invoice || '-'} {asset.import_line || '-'} - -
- - -
-
+ {#if !disabled} + +
+ + +
+
+ {/if}
+ {:else} diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-series.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-series.svelte index 9e93e649..5ede683c 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-series.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-series.svelte @@ -4,22 +4,27 @@ import { Checkbox } from '$lib/components/ui/checkbox'; import { Label } from '$lib/components/ui/label'; import { Button } from '$lib/components/ui/button'; - import { Plus, Pencil, Trash2, CheckCircle2 } from 'lucide-svelte'; - import type { Item, LineDescriptions, Serie } from '$lib/api/dashboard/a76/items'; + import { Plus, Pencil, Trash2, CheckCircle2, AlertTriangle } from 'lucide-svelte'; + import { itemsApi, type Item, type LineDescriptions, type Serie } from '$lib/api/dashboard/a76/items'; import type { Invoice } from '$lib/api/dashboard/a76/invoices'; + import { companyStore } from '$lib/stores/company.svelte'; + import { toast } from 'svelte-sonner'; let { descriptions = $bindable(), series = $bindable(), lineItem, - invoice + invoice, + disabled = false }: { descriptions: LineDescriptions; series: Serie[] | Serie; lineItem: Partial; invoice: Invoice | null; + disabled?: boolean; } = $props(); + // Normalize to array for display and mutations const seriesList = $derived( Array.isArray(series) ? series : series != null ? [series] : [] @@ -70,6 +75,28 @@ } } + async function clearAllSeries() { + if (seriesList.length === 0) return; + + const confirmed = confirm(`¿Estás seguro de que deseas borrar TODAS las series de esta partida? Esta acción no se puede deshacer.`); + if (!confirmed) return; + + try { + // Si la partida ya existe en la DB, llamamos al endpoint de borrado físico + if (lineItem.id && companyStore.activeCompany?.id) { + await itemsApi.deleteSeries(lineItem.id, companyStore.activeCompany.id); + toast.success('Series eliminadas correctamente y registrado en bitácora.'); + } + + // Limpiar el estado local + series = []; + selectedSeriesIndex = null; + } catch (err) { + console.error('Error clearing series:', err); + toast.error('Error al intentar borrar las series.'); + } + } + // Current serie being edited (reference into the array) const currentSerie = $derived( selectedSeriesIndex !== null && seriesList[selectedSeriesIndex] != null @@ -149,21 +176,39 @@ { if (descriptions) descriptions.has_serial = v; }} /> + +
+
+ + +
- @@ -191,9 +236,10 @@ hasSerial && selectForEdit(i)} + : ''} {!hasSerial || disabled ? 'opacity-70' : ''}" + onclick={() => hasSerial && !disabled && selectForEdit(i)} > + {serie.row ?? i + 1} {serie.serial_numbers || '-'} @@ -211,12 +257,13 @@ variant="ghost" size="icon" class="h-7 w-7 text-blue-600" - disabled={!hasSerial} + disabled={disabled || !hasSerial} onclick={(e) => { e.stopPropagation(); - if (hasSerial) selectForEdit(i); + if (hasSerial && !disabled) selectForEdit(i); }} > + @@ -260,12 +308,19 @@ class="h-7 text-xs bg-emerald-600 hover:bg-emerald-700 text-white gap-1" onclick={clearSelection} > - - Aceptar / Listo - - + {#if !disabled} + + {/if} +
@@ -296,9 +351,10 @@ type="number" min={1} step={1} - disabled={!hasSerial} + disabled={disabled || !hasSerial} onblur={clampRowToInteger} /> +
@@ -308,9 +364,10 @@ class="h-8 text-sm focus:ring-primary" maxlength={50} placeholder="Número de serie..." - disabled={!hasSerial} + disabled={disabled || !hasSerial} oninput={(e) => handleSerieInput(e, 'serial_numbers')} /> +
@@ -320,9 +377,10 @@ class="h-8 text-sm focus:ring-primary" maxlength={50} placeholder="Modelo..." - disabled={!hasSerial} + disabled={disabled || !hasSerial} oninput={(e) => handleSerieInput(e, 'model')} /> +
@@ -341,9 +399,10 @@ class="h-8 text-sm focus:ring-primary" maxlength={50} placeholder="Sub modelo..." - disabled={!hasSerial} + disabled={disabled || !hasSerial} oninput={(e) => handleSerieInput(e, 'sub_model')} /> +
@@ -353,23 +412,27 @@ class="h-8 text-sm focus:ring-primary" maxlength={25} placeholder="Número ID..." - disabled={!hasSerial} + disabled={disabled || !hasSerial} oninput={(e) => handleSerieInput(e, 'number_id')} /> +
-
- -
+ {#if !disabled} +
+ +
+ {/if} + {/if} 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 dd6eb2b2..65ad9610 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 @@ -1,5 +1,6 @@