feature/homogenizacion-estilos-catalogos-fijos-y-generales

This commit is contained in:
2026-04-13 11:16:54 -06:00
parent aa8891145c
commit a0a6b52fc1
83 changed files with 4293 additions and 2044 deletions

View File

@@ -40,11 +40,14 @@ import type { ApiResponse } from '$lib/api';
export async function getMultiCurrencyTypes(
companyId: number,
page?: number,
pageSize?: number
pageSize?: number,
filters?: { currency_type_code?: string; country_key?: string }
): Promise<ApiResponse<MultiCurrencyTypeListResponse>> {
const params = new URLSearchParams({ company_id: companyId.toString() });
if (page) params.append('page', page.toString());
if (pageSize) params.append('page_size', pageSize.toString());
if (filters?.currency_type_code) params.append('currency_type_code', filters.currency_type_code);
if (filters?.country_key) params.append('country_key', filters.country_key);
return api.get<MultiCurrencyTypeListResponse>(`/v1/a76/multi-currency-types/?${params.toString()}`);
}

View File

@@ -0,0 +1,152 @@
<script lang="ts" generics="TData, TValue">
import { onMount } from 'svelte';
import { type ColumnDef, getCoreRowModel } from '@tanstack/table-core';
import { createSvelteTable, FlexRender } from '$lib/components/ui/data-table/index.js';
import * as Table from '$lib/components/ui/table/index.js';
type InfiniteDataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
loading: boolean;
hasMore: boolean;
loadMore: () => void;
emptyMessage?: string;
};
let {
data,
columns,
loading,
hasMore,
loadMore,
emptyMessage = 'No hay resultados.'
}: InfiniteDataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel()
});
let scrollContainer = $state<HTMLDivElement>();
let loadingTrigger = $state<HTMLDivElement>();
onMount(() => {
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting && hasMore && !loading) {
loadMore();
}
},
{
root: scrollContainer,
threshold: 0.1
}
);
if (loadingTrigger) {
observer.observe(loadingTrigger);
}
return () => {
observer.disconnect();
};
});
</script>
<div class="flex h-full w-full flex-col overflow-hidden">
<div
class="catalog-table-scroll min-h-[320px] max-h-[calc(100svh-280px)]"
bind:this={scrollContainer}
>
<Table.Root>
<Table.Header class="catalog-table-header">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
{@const headerList = headerGroup.headers}
{@const lastHeaderColId = headerList[headerList.length - 1]?.column.id}
<Table.Row>
{#each headerList as header (header.id)}
{@const colId = header.column.id}
<Table.Head
class={[
'catalog-table-head-cell',
colId === lastHeaderColId &&
'catalog-table-sticky-right z-30'
]
.filter(Boolean)
.join(' ')}
>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#if table.getRowModel().rows.length}
{#each table.getRowModel().rows as row (row.id)}
{@const visibleCells = row.getVisibleCells()}
{@const lastCellColId = visibleCells[visibleCells.length - 1]?.column.id}
<Table.Row
data-state={row.getIsSelected() && 'selected'}
class="catalog-table-row"
>
{#each visibleCells as cell (cell.id)}
{@const colId = cell.column.id}
<Table.Cell
class={[
'whitespace-nowrap',
colId === lastCellColId &&
'catalog-table-sticky-right z-10'
]
.filter(Boolean)
.join(' ')}
>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center text-sm text-muted-foreground">
{emptyMessage}
</Table.Cell>
</Table.Row>
{/if}
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex h-full w-full items-center justify-center">
{#if loading}
<div class="flex items-center justify-center gap-3 rounded-full border bg-muted/30 px-6 py-2 shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-sm font-medium text-foreground">Cargando más registros...</span>
</div>
{:else}
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
</div>

View File

@@ -67,17 +67,18 @@
});
</script>
<div class="w-full">
<div class="w-full h-full">
<div
class="rounded-md border min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto"
class="catalog-table-shell min-h-[360px] max-h-[calc(100svh-420px)] overflow-auto [&_[data-slot=table-container]]:w-full"
bind:this={scrollContainer}
>
<Table.Root>
<Table.Header class="bg-background">
<div class="min-w-max">
<Table.Root>
<Table.Header class="catalog-table-header">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<Table.Head class="catalog-table-head-cell">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
@@ -94,9 +95,7 @@
<Table.Row
data-state={row.getIsSelected() && 'selected'}
onclick={() => onRowClick?.(row.original)}
class="cursor-pointer transition-colors {row.getIsSelected()
? 'bg-gray-300 dark:bg-gray-600'
: 'hover:bg-gray-100 dark:hover:bg-gray-700'}"
class="cursor-pointer {row.getIsSelected() ? 'catalog-table-row-selected' : 'catalog-table-row'}"
>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
@@ -106,7 +105,7 @@
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<Table.Cell colspan={columns.length} class="h-24 text-center text-sm text-muted-foreground">
No hay resultados.
</Table.Cell>
</Table.Row>
@@ -131,6 +130,7 @@
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</Table.Root>
</div>
</div>
</div>

View File

@@ -38,67 +38,69 @@
}
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#if table.getRowModel().rows.length}
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
<div class="flex h-full w-full flex-col overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner">
<Table.Root>
<Table.Header class="sticky top-0 z-20 border-b bg-background/95 shadow-sm backdrop-blur-md">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head class="whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems} registros
</Table.Header>
<Table.Body>
{#if table.getRowModel().rows.length}
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell class="whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 border-t px-4 py-3">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems} registros
</div>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
>
Siguiente
</Button>
</div>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
>
Siguiente
</Button>
</div>

View File

@@ -40,67 +40,69 @@
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
<div class="flex h-full w-full flex-col overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner">
<Table.Root>
<Table.Header class="sticky top-0 z-20 border-b bg-background/95 shadow-sm backdrop-blur-md">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head class="whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell class="whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4 px-2">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems} registros
<div class="flex items-center justify-end space-x-2 border-t px-4 py-3">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems} registros
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(currentPage - 1)}
disabled={currentPage <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(currentPage + 1)}
disabled={currentPage >= pageCount}
>
Siguiente
</Button>
</div>
</div>
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(currentPage - 1)}
disabled={currentPage <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(currentPage + 1)}
disabled={currentPage >= pageCount}
>
Siguiente
</Button>
</div>
</div>

View File

@@ -1,9 +1,8 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import * as Table from '$lib/components/ui/table';
import * as Card from '$lib/components/ui/card';
import { onMount, untrack } from 'svelte';
import { untrack } from 'svelte';
import { toast } from 'svelte-sonner';
import { getSectors, type Sector } from '$lib/api/dashboard/general_catalogs/sectors';
import { Loader2, Search } from 'lucide-svelte';
@@ -103,34 +102,39 @@
});
</script>
<div class="space-y-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<h2 class="text-3xl font-bold tracking-tight">{title}</h2>
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
</div>
<Card.Root>
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
<Card.Header>
<div class="flex w-full items-center gap-4">
<div class="relative flex-1">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar por clave o descripción..."
class="pl-8"
bind:value={searchTerm}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Sectores</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar por clave o descripción..."
class="h-9 w-44 bg-card pl-8 lg:w-64"
bind:value={searchTerm}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
</div>
</div>
</div>
</Card.Header>
<Card.Content>
<div class="rounded-md border">
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-0 flex-1 overflow-auto">
<Table.Root>
<Table.Header>
<Table.Header class="catalog-table-header">
<Table.Row>
<Table.Head class="w-[100px]">Clave</Table.Head>
<Table.Head>Descripción</Table.Head>
<Table.Head class="text-right">Estatus</Table.Head>
<Table.Head class="catalog-table-head-cell w-[100px]">Clave</Table.Head>
<Table.Head class="catalog-table-head-cell">Descripción</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">Estatus</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
@@ -150,7 +154,7 @@
</Table.Row>
{:else}
{#each sectors as sector}
<Table.Row>
<Table.Row class="catalog-table-row">
<Table.Cell class="font-medium">{sector.key}</Table.Cell>
<Table.Cell>{sector.description}</Table.Cell>
<Table.Cell class="text-right">
@@ -174,15 +178,13 @@
{/if}
</Table.Body>
</Table.Root>
</div>
<div class="mt-4 flex flex-col items-center gap-2">
<div class="text-xs text-muted-foreground">
Mostrando {sectors.length} de {total} registros
</div>
<!-- Infinite Scroll Sentinel -->
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {sectors.length} de {total} registros</div>
<!-- Infinite Scroll Sentinel -->
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, untrack } from 'svelte';
import { untrack } from 'svelte';
import {
getCanadianFractions,
deleteCanadianFraction,
@@ -8,6 +8,7 @@
import * as Table from '$lib/components/ui/table';
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { Search, Loader2, Plus, Pencil, Trash2 } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import CanadianFractionDialog from './CanadianFractionDialog.svelte';
@@ -25,6 +26,7 @@
let searchTimeout: ReturnType<typeof setTimeout>;
let observer: IntersectionObserver;
let sentinel: HTMLDivElement;
let scrollContainer: HTMLDivElement;
// Infinite scroll state
let hasMore = $state(true);
@@ -138,7 +140,7 @@
loadFractions(false);
}
},
{ rootMargin: '100px' }
{ root: scrollContainer || null, rootMargin: '100px' }
);
if (sentinel) observer.observe(sentinel);
@@ -162,101 +164,99 @@
});
</script>
<div class="space-y-4">
<div class="flex flex-col items-end justify-between gap-4 md:flex-row">
<div class="flex max-w-2xl flex-1 items-end gap-4">
<div class="flex-1">
<label for="search-fraction" class="mb-2 block text-sm font-medium">Buscar</label>
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-fraction"
type="text"
placeholder="Buscar por fracción o descripción..."
class="pl-9"
bind:value={searchQuery}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden">
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Fracciones Canadienses</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-fraction"
type="text"
placeholder="Buscar por fracción o descripción..."
class="h-9 w-44 bg-card pl-9 lg:w-64"
bind:value={searchQuery}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
</div>
<Button class="h-9" onclick={handleCreate}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
</div>
</div>
</div>
<Button onclick={handleCreate}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
</div>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Fracción</Table.Head>
<Table.Head>Descripción</Table.Head>
<Table.Head>País</Table.Head>
<Table.Head>Unidad</Table.Head>
<Table.Head class="text-right">ADV</Table.Head>
<Table.Head class="w-[100px]">Acciones</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if fractions.length === 0 && !loading}
<Table.Row>
<Table.Cell colspan={6} class="h-24 text-center"
>No se encontraron resultados</Table.Cell
>
</Table.Row>
{:else}
{#each fractions as fraction}
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="catalog-table-header">
<Table.Row>
<Table.Cell class="font-medium">{fraction.fraction}</Table.Cell>
<Table.Cell>{fraction.description || '-'}</Table.Cell>
<Table.Cell>{fraction.country_code}</Table.Cell>
<Table.Cell>{fraction.unit_of_measure || '-'}</Table.Cell>
<Table.Cell class="text-right">{fraction.ad_valorem ?? '-'}</Table.Cell>
<Table.Cell>
<div class="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
onclick={() => handleEdit(fraction)}
>
<Pencil class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => handleDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
<Trash2 class="h-4 w-4" />
{/if}
</Button>
</div>
</Table.Cell>
<Table.Head class="catalog-table-head-cell">Fracción</Table.Head>
<Table.Head class="catalog-table-head-cell">Descripción</Table.Head>
<Table.Head class="catalog-table-head-cell">País</Table.Head>
<Table.Head class="catalog-table-head-cell">Unidad</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">ADV</Table.Head>
<Table.Head class="catalog-table-head-cell w-[100px]">Acciones</Table.Head>
</Table.Row>
{/each}
{/if}
{#if loading}
<Table.Row>
<Table.Cell colspan={6} class="h-24 text-center">
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
</Table.Header>
<Table.Body>
{#if fractions.length === 0 && !loading}
<Table.Row>
<Table.Cell colspan={6} class="h-24 text-center">No se encontraron resultados</Table.Cell>
</Table.Row>
{:else}
{#each fractions as fraction}
<Table.Row class="catalog-table-row">
<Table.Cell class="font-medium">{fraction.fraction}</Table.Cell>
<Table.Cell>{fraction.description || '-'}</Table.Cell>
<TableCell>{fraction.country_code}</TableCell>
<TableCell>{fraction.unit_of_measure || '-'}</TableCell>
<TableCell class="text-right">{fraction.ad_valorem ?? '-'}</TableCell>
<TableCell>
<div class="flex items-center gap-2">
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => handleEdit(fraction)}>
<Pencil class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => handleDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
<Trash2 class="h-4 w-4" />
{/if}
</Button>
</div>
</TableCell>
</Table.Row>
{/each}
{/if}
{#if loading}
<Table.Row>
<Table.Cell colspan={6} class="h-24 text-center">
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>
</div>
</Card.Content>
</Card.Root>
<!-- Infinite Scroll Sentinel -->
<div bind:this={sentinel} class="h-4 w-full"></div>
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalItems} registros</div>
<CanadianFractionDialog
bind:open={dialogOpen}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, untrack } from 'svelte';
import { untrack } from 'svelte';
import {
getHistoricalFractions,
deleteHistoricalFraction,
@@ -8,6 +8,7 @@
import * as Table from '$lib/components/ui/table';
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { Search, Loader2, Plus, Pencil, Trash2 } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import HistoricalFractionDialog from './HistoricalFractionDialog.svelte';
@@ -169,11 +170,12 @@ let scrollContainer: HTMLDivElement;
});
</script>
<div class="flex min-h-[calc(100vh-85px)] flex-col space-y-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<!-- Header -->
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<h2 class="text-2xl font-bold tracking-tight">{title}</h2>
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
<p class="text-sm text-muted-foreground">
Gestiona las fracciones históricas de la tarifa.
</p>
@@ -184,122 +186,100 @@ let scrollContainer: HTMLDivElement;
</Button>
</div>
<!-- Filtros -->
<div class="flex flex-col items-end justify-between gap-4 md:flex-row">
<div class="flex max-w-2xl flex-1 items-end gap-4">
<div class="flex-1">
<label
for="search-fraction"
class="mb-1 block text-xs font-medium text-muted-foreground"
>
Fracción Histórica
</label>
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-fraction"
type="text"
placeholder="Buscar fracción..."
class="h-9 pl-9"
bind:value={historicalFraction}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
<Card.Root class="flex min-h-0 flex-1 flex-col border bg-background">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Fracciones Históricas</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-fraction"
type="text"
placeholder="Buscar fracción..."
class="h-9 w-40 bg-card pl-9 lg:w-56"
bind:value={historicalFraction}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
</div>
</div>
</div>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="catalog-table-header">
<Table.Row>
<Table.Head class="catalog-table-head-cell">Fracción</Table.Head>
<Table.Head class="catalog-table-head-cell">Tipo</Table.Head>
<Table.Head class="catalog-table-head-cell">UM</Table.Head>
<Table.Head class="catalog-table-head-cell">País</Table.Head>
<Table.Head class="catalog-table-head-cell">Fecha Pub.</Table.Head>
<Table.Head class="catalog-table-head-cell">Fecha Fin</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">IGI</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">IGE</Table.Head>
<Table.Head class="catalog-table-head-cell w-[100px]">Acciones</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if fractions.length === 0 && !loading}
<Table.Row>
<Table.Cell colspan={9} class="h-24 text-center">No se encontraron resultados</Table.Cell>
</Table.Row>
{:else}
{#each fractions as fraction}
<Table.Row class="catalog-table-row">
<Table.Cell class="font-medium">{fraction.historical_fraction}</Table.Cell>
<Table.Cell>{fraction.fraction_type || '-'}</Table.Cell>
<Table.Cell>{fraction.unit_of_measure_code || '-'}</Table.Cell>
<Table.Cell>{fraction.country || '-'}</Table.Cell>
<Table.Cell>{fraction.publication_date ? new Date(fraction.publication_date).toLocaleDateString() : '-'}</Table.Cell>
<Table.Cell>{fraction.end_date ? new Date(fraction.end_date).toLocaleDateString() : '-'}</Table.Cell>
<Table.Cell class="text-right">{fraction.import_tax_rate ?? '-'}</Table.Cell>
<Table.Cell class="text-right">{fraction.export_tax_rate ?? '-'}</Table.Cell>
<Table.Cell>
<div class="flex items-center gap-2">
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => handleEdit(fraction)}>
<Pencil class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => handleDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
<Trash2 class="h-4 w-4" />
{/if}
</Button>
</div>
</Table.Cell>
</Table.Row>
{/each}
{/if}
{#if loading}
<Table.Row>
<Table.Cell colspan={9} class="h-24 text-center">
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>
</div>
</Card.Content>
</Card.Root>
<!-- Tabla con scroll interno -->
<div class="flex min-h-0 flex-1 flex-col rounded-md border">
<div
class="min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto"
bind:this={scrollContainer}
>
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Fracción</Table.Head>
<Table.Head>Tipo</Table.Head>
<Table.Head>UM</Table.Head>
<Table.Head>País</Table.Head>
<Table.Head>Fecha Pub.</Table.Head>
<Table.Head>Fecha Fin</Table.Head>
<Table.Head class="text-right">IGI</Table.Head>
<Table.Head class="text-right">IGE</Table.Head>
<Table.Head class="w-[100px]">Acciones</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if fractions.length === 0 && !loading}
<Table.Row>
<Table.Cell colspan={9} class="h-24 text-center"
>No se encontraron resultados</Table.Cell
>
</Table.Row>
{:else}
{#each fractions as fraction}
<Table.Row>
<Table.Cell class="font-medium">{fraction.historical_fraction}</Table.Cell>
<Table.Cell>{fraction.fraction_type || '-'}</Table.Cell>
<Table.Cell>{fraction.unit_of_measure_code || '-'}</Table.Cell>
<Table.Cell>{fraction.country || '-'}</Table.Cell>
<Table.Cell
>{fraction.publication_date
? new Date(fraction.publication_date).toLocaleDateString()
: '-'}</Table.Cell
>
<Table.Cell
>{fraction.end_date
? new Date(fraction.end_date).toLocaleDateString()
: '-'}</Table.Cell
>
<Table.Cell class="text-right">{fraction.import_tax_rate ?? '-'}</Table.Cell>
<Table.Cell class="text-right">{fraction.export_tax_rate ?? '-'}</Table.Cell>
<Table.Cell>
<div class="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
onclick={() => handleEdit(fraction)}
>
<Pencil class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => handleDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
<Trash2 class="h-4 w-4" />
{/if}
</Button>
</div>
</Table.Cell>
</Table.Row>
{/each}
{/if}
{#if loading}
<Table.Row>
<Table.Cell colspan={9} class="h-24 text-center">
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
<!-- Infinite Scroll Sentinel dentro del contenedor con scroll -->
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>
</div>
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalItems} registros</div>
<HistoricalFractionDialog
bind:open={dialogOpen}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label';
import * as Card from '$lib/components/ui/card';
import {
Table,
TableBody,
@@ -17,7 +17,7 @@
type TariffFraction
} from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
import { companyStore } from '$lib/stores/company.svelte';
import { onMount, untrack } from 'svelte';
import { untrack } from 'svelte';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import TariffFractionFormDialog from './TariffFractionFormDialog.svelte';
import { toast } from 'svelte-sonner';
@@ -43,6 +43,7 @@
let searchTimeout: ReturnType<typeof setTimeout>;
let observer: IntersectionObserver;
let sentinel: HTMLDivElement;
let scrollContainer: HTMLDivElement;
// Infinite scroll state
let hasMore = $state(true);
@@ -121,7 +122,7 @@
handlePageChange(currentPage + 1);
}
},
{ rootMargin: '100px' }
{ root: scrollContainer || null, rootMargin: '100px' }
);
if (sentinel) observer.observe(sentinel);
@@ -180,108 +181,118 @@
});
</script>
<div class="space-y-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-bold tracking-tight">{title}</h2>
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
{#if !readOnly}
<Button onclick={openCreateDialog}>
<Button class="h-9" onclick={openCreateDialog}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
{/if}
</div>
<div class="flex items-center space-x-2">
<div class="relative max-w-sm flex-1">
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
<Input placeholder="Buscar..." class="pl-8" bind:value={search} oninput={handleSearchInput} />
</div>
</div>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Clave</TableHead>
<TableHead>Fracción</TableHead>
<TableHead>Descripción</TableHead>
{#if catalog === 'mex'}
<TableHead>NICO</TableHead>
<TableHead>U.M.T</TableHead>
{:else}
<TableHead>Unidad</TableHead>
{/if}
<TableHead>Adv. Impo</TableHead>
<TableHead>Adv. Expo</TableHead>
{#if !readOnly}
<TableHead class="w-[100px]">Acciones</TableHead>
{/if}
</TableRow>
</TableHeader>
<TableBody>
{#if fractions.length === 0 && !isLoading}
<TableRow>
<TableCell
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
class="h-24 text-center"
>
No se encontraron resultados
</TableCell>
</TableRow>
{:else}
{#each fractions as fraction}
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Fracciones</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
<Input placeholder="Buscar..." class="h-9 w-40 bg-card pl-8 lg:w-56" bind:value={search} oninput={handleSearchInput} />
</div>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-auto" bind:this={scrollContainer}>
<Table>
<TableHeader class="catalog-table-header">
<TableRow>
<TableCell class="font-mono">{fraction.um_code || fraction.code}</TableCell>
<TableCell class="font-medium">{fraction.fraction}</TableCell>
<TableCell class="max-w-md truncate" title={fraction.description}>
{fraction.description}
</TableCell>
<TableHead class="catalog-table-head-cell">Clave</TableHead>
<TableHead class="catalog-table-head-cell">Fracción</TableHead>
<TableHead class="catalog-table-head-cell">Descripción</TableHead>
{#if catalog === 'mex'}
<TableCell>{fraction.nico || '-'}</TableCell>
<TableCell>{fraction.umt || '-'}</TableCell>
<TableHead class="catalog-table-head-cell">NICO</TableHead>
<TableHead class="catalog-table-head-cell">U.M.T</TableHead>
{:else}
<TableCell>{fraction.umt || '-'}</TableCell>
<TableHead class="catalog-table-head-cell">Unidad</TableHead>
{/if}
<TableCell>{fraction.adv_impo || '-'}</TableCell>
<TableCell>{fraction.adv_expo || '-'}</TableCell>
<TableHead class="catalog-table-head-cell">Adv. Impo</TableHead>
<TableHead class="catalog-table-head-cell">Adv. Expo</TableHead>
{#if !readOnly}
<TableCell>
<div class="flex items-center gap-2">
<Button variant="ghost" size="icon" onclick={() => openEditDialog(fraction)}>
<Edit class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="text-destructive hover:text-destructive"
onclick={() => confirmDelete(fraction)}
>
<Trash2 class="h-4 w-4" />
</Button>
</div>
</TableCell>
<TableHead class="catalog-table-head-cell w-[100px]">Acciones</TableHead>
{/if}
</TableRow>
{/each}
{/if}
{#if isLoading}
<TableRow>
<TableCell
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
class="h-24 text-center"
>
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</TableCell>
</TableRow>
{/if}
</TableBody>
</Table>
</div>
</TableHeader>
<TableBody>
{#if fractions.length === 0 && !isLoading}
<TableRow>
<TableCell
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
class="h-24 text-center"
>
No se encontraron resultados
</TableCell>
</TableRow>
{:else}
{#each fractions as fraction}
<TableRow class="catalog-table-row">
<TableCell class="font-mono">{fraction.um_code || fraction.code}</TableCell>
<TableCell class="font-medium">{fraction.fraction}</TableCell>
<TableCell class="max-w-md truncate" title={fraction.description}>
{fraction.description}
</TableCell>
{#if catalog === 'mex'}
<TableCell>{fraction.nico || '-'}</TableCell>
<TableCell>{fraction.umt || '-'}</TableCell>
{:else}
<TableCell>{fraction.umt || '-'}</TableCell>
{/if}
<TableCell>{fraction.adv_impo || '-'}</TableCell>
<TableCell>{fraction.adv_expo || '-'}</TableCell>
{#if !readOnly}
<TableCell>
<div class="flex items-center gap-2">
<Button variant="ghost" size="icon" onclick={() => openEditDialog(fraction)}>
<Edit class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="text-destructive hover:text-destructive"
onclick={() => confirmDelete(fraction)}
>
<Trash2 class="h-4 w-4" />
</Button>
</div>
</TableCell>
{/if}
</TableRow>
{/each}
{/if}
{#if isLoading}
<TableRow>
<TableCell
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
class="h-24 text-center"
>
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</TableCell>
</TableRow>
{/if}
</TableBody>
</Table>
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>
</div>
</Card.Content>
</Card.Root>
<!-- Infinite Scroll Sentinel -->
<div bind:this={sentinel} class="h-4 w-full"></div>
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalFractions} registros</div>
</div>
<AlertDialog.Root bind:open={showDeleteConfirm}>

View File

@@ -99,9 +99,13 @@
</script>
<div class="w-full">
<div class="min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto rounded-md border" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div
class="catalog-table-shell min-h-[360px] max-h-[calc(100svh-420px)] overflow-auto [&_[data-slot=table-container]]:overflow-visible [&_[data-slot=table-container]]:w-full"
bind:this={scrollContainer}
>
<div class="min-w-max">
<Table.Root>
<Table.Header class="catalog-table-header">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
{@const headerList = headerGroup.headers}
{@const lastHeaderColId = headerList[headerList.length - 1]?.column.id}
@@ -111,9 +115,9 @@
<Table.Head
class={[
colId === 'select' &&
'sticky left-0 z-40 min-w-[2.75rem] border-r border-border/70 bg-background shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]',
'catalog-table-sticky-left z-40 min-w-[2.75rem]',
colId === lastHeaderColId &&
'sticky right-0 z-40 border-l border-border/70 bg-background shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.35)]'
'catalog-table-sticky-right z-40'
]
.filter(Boolean)
.join(' ')}
@@ -192,9 +196,7 @@
{@const lastCellColId = visibleCells[visibleCells.length - 1]?.column.id}
<Table.Row
data-state={row.getIsSelected() && 'selected'}
class="group/inv-list cursor-pointer transition-colors {row.getIsSelected()
? 'bg-gray-300 dark:bg-gray-600'
: 'hover:bg-gray-100 dark:hover:bg-gray-700'}"
class="group/inv-list cursor-pointer {row.getIsSelected() ? 'catalog-table-row-selected' : 'catalog-table-row'}"
onclick={() => onRowClick && onRowClick(row.original)}
>
{#each visibleCells as cell (cell.id)}
@@ -202,12 +204,12 @@
<Table.Cell
class={[
colId === 'select' &&
'sticky left-0 z-30 min-w-[2.75rem] border-r border-border/70 shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]',
'catalog-table-sticky-left z-30 min-w-[2.75rem]',
colId === lastCellColId &&
'sticky right-0 z-30 border-l border-border/70 shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.35)]',
'catalog-table-sticky-right z-30',
row.getIsSelected()
? 'bg-gray-300 dark:bg-gray-600'
: 'bg-background group-hover/inv-list:bg-gray-100 dark:group-hover/inv-list:bg-gray-700'
? 'catalog-table-sticky-row-selected'
: 'catalog-table-sticky-row-hover'
]
.filter(Boolean)
.join(' ')}
@@ -244,6 +246,7 @@
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</Table.Root>
</div>
</div>
</div>

View File

@@ -98,18 +98,24 @@
<div class="w-full">
<div
class="rounded-md border min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto"
class="min-h-[360px] max-h-[calc(100svh-420px)] overflow-auto rounded-md border bg-background [&_[data-slot=table-container]]:overflow-visible [&_[data-slot=table-container]]:w-full"
bind:this={scrollContainer}
>
<Table.Root class="w-full">
<Table.Header class="bg-background sticky top-0 z-10">
<div class="min-w-max">
<Table.Root>
<Table.Header class="sticky top-0 z-20 border-b bg-background/95 shadow-sm backdrop-blur-md">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head class="whitespace-nowrap p-0 {header.column.columnDef.meta?.className || ''}">
{@const colId = header.column.id}
<Table.Head
class="whitespace-nowrap text-sm font-semibold text-foreground/90 p-0 {header.column.columnDef.meta?.className || ''} {colId === 'select'
? 'sticky left-0 z-40 min-w-[2.75rem] border-r border-border/70 bg-background shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]'
: ''}"
>
{#if !header.isPlaceholder}
<button
class="group flex h-full w-full items-center gap-2 px-2 py-2 text-left hover:bg-muted/50"
class="group flex h-full w-full items-center justify-between gap-2 px-2 py-2 text-left {header.column.getCanSort() ? 'cursor-pointer select-none' : ''}"
onclick={header.column.getToggleSortingHandler()}
disabled={!header.column.getCanSort()}
>
@@ -181,10 +187,19 @@
}
}}
ondblclick={() => handleRowDoubleClick(row)}
class="cursor-pointer hover:bg-muted/50 transition-colors {row.getIsSelected() ? 'bg-primary/10' : ''}"
class="group/inv-list cursor-pointer transition-colors {row.getIsSelected()
? 'bg-gray-300 dark:bg-gray-600'
: 'hover:bg-gray-100 dark:hover:bg-gray-700'}"
>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell class="whitespace-nowrap {cell.column.columnDef.meta?.className || ''}">
{@const colId = cell.column.id}
<Table.Cell
class="whitespace-nowrap {cell.column.columnDef.meta?.className || ''} {colId === 'select'
? 'sticky left-0 z-30 min-w-[2.75rem] border-r border-border/70 bg-background shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]'
: ''} {row.getIsSelected()
? 'bg-gray-300 dark:bg-gray-600'
: 'bg-background group-hover/inv-list:bg-gray-100 dark:group-hover/inv-list:bg-gray-700'}"
>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
@@ -220,6 +235,7 @@
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</Table.Root>
</div>
</div>
</div>

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -51,7 +51,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -60,7 +60,7 @@
</script>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -61,7 +61,7 @@
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -18,8 +18,8 @@
let { data, columns, loading = false, hasMore = false, loadMore }: Props = $props();
let scrollContainer: HTMLDivElement;
let observer: IntersectionObserver;
let scrollContainer = $state<HTMLDivElement>();
let loadingTrigger = $state<HTMLDivElement>();
let options = $derived<TableOptions<TData>>({
get data() {
@@ -34,8 +34,7 @@
onMount(() => {
if (!loadMore) return;
// Create intersection observer for infinite scroll
observer = new IntersectionObserver(
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting && hasMore && !loading && loadMore) {
@@ -48,29 +47,24 @@
}
);
// Observe the last row
const lastRow = scrollContainer?.querySelector('tbody tr:last-child');
if (lastRow) {
observer.observe(lastRow);
if (loadingTrigger) {
observer.observe(loadingTrigger);
}
return () => {
observer?.disconnect();
observer.disconnect();
};
});
</script>
<div class="w-full">
<div
class="rounded-md border min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto"
bind:this={scrollContainer}
>
<div class="flex h-full w-full flex-col overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<Table.Header class="sticky top-0 z-20 border-b bg-background/95 shadow-sm backdrop-blur-md">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<Table.Head class="whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
@@ -86,7 +80,7 @@
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<Table.Cell class="whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
@@ -101,11 +95,24 @@
</Table.Cell>
</Table.Row>
{/each}
{#if loading}
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-12 text-center text-muted-foreground">
Cargando...
<Table.Cell colspan={columns.length} class="h-24 p-0 text-center">
<div bind:this={loadingTrigger} class="flex h-full w-full items-center justify-center">
{#if loading}
<div class="flex items-center justify-center gap-3 rounded-full border bg-muted/30 px-6 py-2 shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-sm font-medium text-foreground">Cargando más registros...</span>
</div>
{:else}
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
{/if}