diff --git a/frontend/src/routes/dashboard/clients_and_providers/+page.svelte b/frontend/src/routes/dashboard/clients_and_providers/+page.svelte index 8384d1b7..8f6f236d 100644 --- a/frontend/src/routes/dashboard/clients_and_providers/+page.svelte +++ b/frontend/src/routes/dashboard/clients_and_providers/+page.svelte @@ -21,7 +21,7 @@ let { data }: { data: any } = $props(); // State - let items = $state(data.items || []); + let allItems = $state(data.items || []); let selectedItem = $state(null); let isLoading = $state(false); @@ -29,11 +29,16 @@ let currentPage = $state(data.page || 1); let pageSize = $state(50); let totalItems = $state(data.total || 0); + let hasMore = $derived(allItems.length < totalItems); // Filter state let searchName = $state(''); let searchRfc = $state(''); let searchType = $state($page.url.searchParams.get('type') || 'both'); + let searchTimeout: ReturnType; + let hasMounted = false; + let scrollContainer = $state(); + let loadingTrigger = $state(); // Estado para el diálogo de crear let showCreateDialog = $state(false); @@ -42,6 +47,7 @@ // --- Lifecycle --- onMount(() => { + hasMounted = true; if (browser) { // Sincronizar token de cookies a localStorage si es necesario const getCookie = (name: string) => { @@ -62,9 +68,41 @@ } }); + $effect(() => { + const type = searchType; + const name = searchName; + const rfc = searchRfc; + if (!browser || !hasMounted) return; + + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + void loadItems(1); + }, 400); + + return () => clearTimeout(searchTimeout); + }); + + $effect(() => { + if (!browser || !scrollContainer || !loadingTrigger) return; + const observer = new IntersectionObserver( + (entries) => { + const [entry] = entries; + if (entry.isIntersecting && hasMore && !isLoading) { + void loadMore(); + } + }, + { + root: scrollContainer, + threshold: 0.1 + } + ); + observer.observe(loadingTrigger); + return () => observer.disconnect(); + }); + // --- Actions --- - async function loadItems(pageToLoad = 1) { + async function loadItems(pageToLoad = 1, append = false) { const companyId = companyStore.activeCompany?.id; if (!companyId) return; @@ -93,7 +131,7 @@ } if (response.data) { - items = response.data.items; + allItems = append ? [...allItems, ...response.data.items] : response.data.items; totalItems = response.data.total; currentPage = response.data.page; } @@ -107,17 +145,22 @@ function handleTypeChange(value: string) { searchType = value; - loadItems(1); } - function handleSearch() { - loadItems(1); + async function loadMore() { + if (isLoading || !hasMore) return; + await loadItems(currentPage + 1, true); } function selectItem(item: ClientProvider) { selectedItem = item; } + function handleRowDoubleClick(item: ClientProvider) { + selectedItem = item; + goto(`/dashboard/clients_and_providers/edit/${item.id}`); + } + function taxIdOrRfcLabel(cp: ClientProvider | null): string { if (!cp) return 'RFC'; const proc = (cp.type_nat_foreign || 'N').toUpperCase(); @@ -139,7 +182,7 @@ await clientsProvidersApi.delete(selectedItem.id, companyStore.activeCompany.id); toast.success('Registro eliminado'); selectedItem = null; - loadItems(currentPage); + loadItems(1); } catch (e) { toast.error('Error al eliminar'); } @@ -197,7 +240,6 @@ bind:value={searchName} placeholder="Buscar por nombre..." class="h-9" - onkeydown={(e) => e.key === 'Enter' && handleSearch()} />
@@ -206,7 +248,6 @@ bind:value={searchRfc} placeholder="RFC / TAX-ID..." class="h-9" - onkeydown={(e) => e.key === 'Enter' && handleSearch()} />
@@ -227,9 +268,9 @@
- +
+ Los filtros se aplican automáticamente +
@@ -250,7 +291,7 @@ -
+
@@ -262,25 +303,26 @@ - {#if isLoading} + {#if isLoading && allItems.length === 0} - {:else if items.length === 0} + {:else if allItems.length === 0} {:else} - {#each items as item (item.id)} + {#each allItems as item (item.id)} selectItem(item)} + ondblclick={() => handleRowDoubleClick(item)} > @@ -315,30 +357,26 @@ {/each} {/if} + {#if hasMore && allItems.length > 0} + + + + {/if}
Cargando...
No se encontraron registros
{item.id} {item.rfc}
+
+ {#if isLoading} + Cargando más registros... + {:else} + Desplázate para cargar más + {/if} +
+
- -
- +
- Página {currentPage} de {Math.ceil(totalItems / pageSize)} + Mostrando {allItems.length} de {totalItems} -