feature/mejora-cliente-proveedor-scroll-filtros
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
let { data }: { data: any } = $props();
|
||||
|
||||
// State
|
||||
let items = $state<ClientProvider[]>(data.items || []);
|
||||
let allItems = $state<ClientProvider[]>(data.items || []);
|
||||
let selectedItem = $state<ClientProvider | null>(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<string>($page.url.searchParams.get('type') || 'both');
|
||||
let searchTimeout: ReturnType<typeof setTimeout>;
|
||||
let hasMounted = false;
|
||||
let scrollContainer = $state<HTMLDivElement>();
|
||||
let loadingTrigger = $state<HTMLDivElement>();
|
||||
|
||||
// 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()}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@@ -206,7 +248,6 @@
|
||||
bind:value={searchRfc}
|
||||
placeholder="RFC / TAX-ID..."
|
||||
class="h-9"
|
||||
onkeydown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@@ -227,9 +268,9 @@
|
||||
</Select.Root>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<Button variant="secondary" size="sm" class="w-full" onclick={handleSearch}>
|
||||
Buscar
|
||||
</Button>
|
||||
<div class="text-xs text-muted-foreground pb-2">
|
||||
Los filtros se aplican automáticamente
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,7 +291,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto bg-card">
|
||||
<div class="flex-1 overflow-auto bg-card" bind:this={scrollContainer}>
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted text-muted-foreground border-b">
|
||||
<tr>
|
||||
@@ -262,25 +303,26 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if isLoading}
|
||||
{#if isLoading && allItems.length === 0}
|
||||
<tr
|
||||
><td colspan="5" class="text-center py-8 text-muted-foreground">Cargando...</td
|
||||
></tr
|
||||
>
|
||||
{:else if items.length === 0}
|
||||
{:else if allItems.length === 0}
|
||||
<tr
|
||||
><td colspan="5" class="text-center py-8 text-muted-foreground"
|
||||
>No se encontraron registros</td
|
||||
></tr
|
||||
>
|
||||
{:else}
|
||||
{#each items as item (item.id)}
|
||||
{#each allItems as item (item.id)}
|
||||
<tr
|
||||
class="border-b cursor-pointer transition-colors hover:bg-muted/50 {selectedItem?.id ===
|
||||
item.id
|
||||
? 'bg-muted'
|
||||
: ''}"
|
||||
onclick={() => selectItem(item)}
|
||||
ondblclick={() => handleRowDoubleClick(item)}
|
||||
>
|
||||
<td class="px-3 py-2 font-mono text-xs text-muted-foreground">{item.id}</td>
|
||||
<td class="px-3 py-2 font-mono font-medium">{item.rfc}</td>
|
||||
@@ -315,30 +357,26 @@
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if hasMore && allItems.length > 0}
|
||||
<tr>
|
||||
<td colspan="5" class="p-0">
|
||||
<div bind:this={loadingTrigger} class="flex items-center justify-center py-5">
|
||||
{#if isLoading}
|
||||
<span class="text-xs text-muted-foreground">Cargando más registros...</span>
|
||||
{:else}
|
||||
<span class="text-xs text-muted-foreground">Desplázate para cargar más</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Simple Pagination Controls -->
|
||||
<div class="p-2 border-t flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage === 1 || isLoading}
|
||||
onclick={() => loadItems(currentPage - 1)}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<div class="p-2 border-t flex justify-end">
|
||||
<span class="flex items-center text-xs text-muted-foreground px-2">
|
||||
Página {currentPage} de {Math.ceil(totalItems / pageSize)}
|
||||
Mostrando {allItems.length} de {totalItems}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={items.length < pageSize || isLoading}
|
||||
onclick={() => loadItems(currentPage + 1)}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user