From 1ce8f05ee0e89cac2b02eaf3d396e7ef138e6f0b Mon Sep 17 00:00:00 2001 From: hreyes Date: Wed, 29 Apr 2026 09:02:26 -0600 Subject: [PATCH 1/2] feature/mejora-cliente-proveedor-scroll-filtros --- .../clients_and_providers/+page.svelte | 108 ++++++++++++------ 1 file changed, 73 insertions(+), 35 deletions(-) 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} -
From 1f63506b4a4d1f98e3092a8f0673658102cd04f6 Mon Sep 17 00:00:00 2001 From: hreyes Date: Wed, 29 Apr 2026 09:15:30 -0600 Subject: [PATCH 2/2] feature/tabla-errores --- .../components/ui/error-panel-notice.svelte | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/frontend/src/lib/components/ui/error-panel-notice.svelte b/frontend/src/lib/components/ui/error-panel-notice.svelte index e69de29b..996f4db3 100644 --- a/frontend/src/lib/components/ui/error-panel-notice.svelte +++ b/frontend/src/lib/components/ui/error-panel-notice.svelte @@ -0,0 +1,203 @@ + + +{#if open} +
+ +
+ +

{title}

+ + + + +
+ + {#if expanded && rows.length > 0} +
+ + + + + + + {#if dismissibleRows && onDismissRow} + + {/if} + + + + {#each rows as row, i (i)} + + + + + {#if dismissibleRows && onDismissRow} + + {/if} + + {/each} + +
+ {labels.columnType} + + {labels.columnField} + + {labels.columnMessage} +
+ + + {row.field || labels.emptyField} + + {row.message} + + +
+
+ {/if} +
+{/if}