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}
+
+
+
+
+ {#if isWarning}
+
+ {:else}
+
+ {/if}
+
+
{title}
+
+
+
+
+
+
+ {#if expanded && rows.length > 0}
+
+
+
+
+ |
+ {labels.columnType}
+ |
+
+ {labels.columnField}
+ |
+
+ {labels.columnMessage}
+ |
+ {#if dismissibleRows && onDismissRow}
+ |
+ {/if}
+
+
+
+ {#each rows as row, i (i)}
+
+ |
+
+ {#if isWarning}
+
+ {:else}
+ ×
+ {/if}
+
+ |
+
+ {row.field || labels.emptyField}
+ |
+
+ {row.message}
+ |
+ {#if dismissibleRows && onDismissRow}
+
+
+ |
+ {/if}
+
+ {/each}
+
+
+
+ {/if}
+
+{/if}
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}
| Cargando... |
- {:else if items.length === 0}
+ {:else if allItems.length === 0}
| No se encontraron registros |
{:else}
- {#each items as item (item.id)}
+ {#each allItems as item (item.id)}
selectItem(item)}
+ ondblclick={() => handleRowDoubleClick(item)}
>
| {item.id} |
{item.rfc} |
@@ -315,30 +357,26 @@
{/each}
{/if}
+ {#if hasMore && allItems.length > 0}
+
+ |
+
+ {#if isLoading}
+ Cargando más registros...
+ {:else}
+ Desplázate para cargar más
+ {/if}
+
+ |
+
+ {/if}
-
-
-
+
- Página {currentPage} de {Math.ceil(totalItems / pageSize)}
+ Mostrando {allItems.length} de {totalItems}
-