diff --git a/frontend/src/lib/components/dashboard/clients_and_providers/data-table.svelte b/frontend/src/lib/components/dashboard/clients_and_providers/data-table.svelte index 2f4a88e7..4255092c 100644 --- a/frontend/src/lib/components/dashboard/clients_and_providers/data-table.svelte +++ b/frontend/src/lib/components/dashboard/clients_and_providers/data-table.svelte @@ -13,6 +13,8 @@ loading: boolean; hasMore: boolean; loadMore: () => void; + onRowClick?: (row: TData) => void; + selectedId?: number | null; }; let { @@ -20,7 +22,9 @@ columns, loading, hasMore, - loadMore + loadMore, + onRowClick, + selectedId = null }: DataTableProps = $props(); const table = createSvelteTable({ @@ -33,9 +37,17 @@ let scrollContainer = $state(); let loadingTrigger = $state(); + let tableContainer = $state(); + let bottomScrollbar = $state(); + let bottomScrollbarInner = $state(); + let isSyncingHorizontalScroll = false; // Intersection Observer para detectar cuando el usuario llega al final - onMount(() => { + $effect(() => { + const target = loadingTrigger; + const root = scrollContainer; + if (!target) return; + const observer = new IntersectionObserver( (entries) => { const [entry] = entries; @@ -44,32 +56,77 @@ } }, { - root: scrollContainer, + root: root, threshold: 0.1 } ); - if (loadingTrigger) { - observer.observe(loadingTrigger); - } + observer.observe(target); return () => { observer.disconnect(); }; }); + + function syncBottomScrollbarWidth() { + if (!tableContainer || !bottomScrollbarInner) return; + const width = Math.max(tableContainer.scrollWidth || 0, tableContainer.clientWidth + 1); + bottomScrollbarInner.style.width = `${width}px`; + } + + function handleTableHorizontalScroll() { + if (!tableContainer || !bottomScrollbar || isSyncingHorizontalScroll) return; + isSyncingHorizontalScroll = true; + bottomScrollbar.scrollLeft = tableContainer.scrollLeft; + isSyncingHorizontalScroll = false; + } + + function handleBottomHorizontalScroll() { + if (!tableContainer || !bottomScrollbar || isSyncingHorizontalScroll) return; + isSyncingHorizontalScroll = true; + tableContainer.scrollLeft = bottomScrollbar.scrollLeft; + isSyncingHorizontalScroll = false; + } + + onMount(() => { + tableContainer = scrollContainer?.querySelector('[data-slot="table-container"]') ?? undefined; + if (!tableContainer) return; + + const resizeObserver = new ResizeObserver(() => { + syncBottomScrollbarWidth(); + }); + + tableContainer.addEventListener('scroll', handleTableHorizontalScroll, { passive: true }); + resizeObserver.observe(tableContainer); + const tableEl = tableContainer.querySelector('[data-slot="table"]'); + if (tableEl) resizeObserver.observe(tableEl); + + syncBottomScrollbarWidth(); + + return () => { + tableContainer?.removeEventListener('scroll', handleTableHorizontalScroll); + resizeObserver.disconnect(); + }; + }); + + $effect(() => { + data; + columns; + queueMicrotask(() => syncBottomScrollbarWidth()); + });
- - + + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} {#each headerGroup.headers as header (header.id)} - + {#if !header.isPlaceholder} {#each table.getRowModel().rows as row (row.id)} - + onRowClick && onRowClick(row.original)} + > {#each row.getVisibleCells() as cell (cell.id)} - +
+
+
+
diff --git a/frontend/src/lib/components/dashboard/export/manifest/data-table.svelte b/frontend/src/lib/components/dashboard/export/manifest/data-table.svelte index ec6893aa..cbbda2c4 100644 --- a/frontend/src/lib/components/dashboard/export/manifest/data-table.svelte +++ b/frontend/src/lib/components/dashboard/export/manifest/data-table.svelte @@ -44,8 +44,11 @@ let scrollContainer = $state(); let loadingTrigger = $state(); - onMount(() => { + $effect(() => { if (!loadMore) return; + const target = loadingTrigger; + const root = scrollContainer; + if (!target) return; const observer = new IntersectionObserver( (entries) => { @@ -55,14 +58,12 @@ } }, { - root: null, // Relative to viewport if scrollContainer is used as max-h div + root: root, threshold: 0.1 } ); - if (loadingTrigger) { - observer.observe(loadingTrigger); - } + observer.observe(target); return () => { observer.disconnect(); diff --git a/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte b/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte index e6c1e89c..1ce39545 100644 --- a/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte +++ b/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte @@ -72,7 +72,11 @@ } // Intersection Observer para detectar cuando el usuario llega al final - onMount(() => { + $effect(() => { + const target = loadingTrigger; + const root = scrollContainer; + if (!target) return; + const observer = new IntersectionObserver( (entries) => { const [entry] = entries; @@ -81,14 +85,12 @@ } }, { - root: scrollContainer, + root: root, threshold: 0.1 } ); - if (loadingTrigger) { - observer.observe(loadingTrigger); - } + observer.observe(target); return () => { observer.disconnect(); diff --git a/frontend/src/lib/components/dashboard/reference_data/countries/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/countries/data-table.svelte index 1e44175d..261494f0 100644 --- a/frontend/src/lib/components/dashboard/reference_data/countries/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/countries/data-table.svelte @@ -35,7 +35,11 @@ let loadingTrigger = $state(); // Intersection Observer para detectar cuando el usuario llega al final - onMount(() => { + $effect(() => { + const target = loadingTrigger; + const root = scrollContainer; + if (!target) return; + const observer = new IntersectionObserver( (entries) => { const [entry] = entries; @@ -44,14 +48,12 @@ } }, { - root: scrollContainer, + root: root, threshold: 0.1 } ); - if (loadingTrigger) { - observer.observe(loadingTrigger); - } + observer.observe(target); return () => { observer.disconnect(); diff --git a/frontend/src/routes/dashboard/clients_and_providers/+page.svelte b/frontend/src/routes/dashboard/clients_and_providers/+page.svelte index 8384d1b7..758f1fb7 100644 --- a/frontend/src/routes/dashboard/clients_and_providers/+page.svelte +++ b/frontend/src/routes/dashboard/clients_and_providers/+page.svelte @@ -16,6 +16,8 @@ import { toast } from 'svelte-sonner'; import { companyStore } from '$lib/stores/company.svelte'; import type { ApiError } from '$lib/utils/error-handler'; + import DataTable from '$lib/components/dashboard/clients_and_providers/data-table.svelte'; + import { createColumns } from '$lib/components/dashboard/clients_and_providers/columns'; // Los datos iniciales vienen del servidor let { data }: { data: any } = $props(); @@ -24,6 +26,7 @@ let items = $state(data.items || []); let selectedItem = $state(null); let isLoading = $state(false); + let hasMore = $derived(items.length < totalItems); // Server-side filtering/pagination parameters let currentPage = $state(data.page || 1); @@ -64,7 +67,7 @@ // --- Actions --- - async function loadItems(pageToLoad = 1) { + async function loadItems(pageToLoad = 1, append = false) { const companyId = companyStore.activeCompany?.id; if (!companyId) return; @@ -72,11 +75,6 @@ try { const filters: any = {}; if (searchType !== 'both') filters.type = searchType; - // Note: The API technically supports name/rfc fitlering if backend implements it. - // Assuming backend supports 'name' and 'rfc' query params based on standard patterns, - // or we filter client side if the list is small. - // Given pagination, we should try sending them. If backend ignores them, we might need client filtering. - // Ideally backend should handle this. I will assume backend filters for now or add query params. if (searchName) filters.name = searchName; if (searchRfc) filters.rfc = searchRfc; @@ -93,7 +91,11 @@ } if (response.data) { - items = response.data.items; + if (append) { + items = [...items, ...response.data.items]; + } else { + items = response.data.items; + } totalItems = response.data.total; currentPage = response.data.page; } @@ -105,6 +107,11 @@ } } + async function loadMore() { + if (isLoading || !hasMore) return; + await loadItems(currentPage + 1, true); + } + function handleTypeChange(value: string) { searchType = value; loadItems(1); @@ -250,95 +257,16 @@ -
- - - - - - - - - - - - {#if isLoading} - - {:else if items.length === 0} - - {:else} - {#each items as item (item.id)} - selectItem(item)} - > - - - - - - - {/each} - {/if} - -
#RFC / TAX-IDNombreTipoEstatus
Cargando...
No se encontraron registros
{item.id}{item.rfc}{item.name} - {#if item.client_or_provider === 'client'} - Cliente - {:else if item.client_or_provider === 'provider'} - Proveedor - {:else} - Ambos - {/if} - - - {item.is_active ? 'Activo' : 'Inactivo'} - -
-
- -
- - - Página {currentPage} de {Math.ceil(totalItems / pageSize)} - - +
+ loadItems(1))} + loading={isLoading} + {hasMore} + {loadMore} + onRowClick={(row) => selectItem(row as ClientProvider)} + selectedId={selectedItem?.id} + />