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} +