feature/bug-no-scroll-input-classes
This commit is contained in:
@@ -48,18 +48,30 @@ class ClassService:
|
||||
query = query.filter(Class.company_id == company_id)
|
||||
|
||||
if filters:
|
||||
if filters.get("class_code"):
|
||||
query = query.filter(
|
||||
Class.class_code.ilike(f"%{filters['class_code']}%")
|
||||
)
|
||||
if filters.get("description"):
|
||||
description_pattern = f"%{filters['description']}%"
|
||||
# Búsqueda libre: OR en clave y descripciones (selectores / listados)
|
||||
search_raw = filters.get("search") or filters.get("q")
|
||||
if search_raw and str(search_raw).strip():
|
||||
pattern = f"%{str(search_raw).strip()}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
Class.description_es.ilike(description_pattern),
|
||||
Class.description_en.ilike(description_pattern),
|
||||
Class.class_code.ilike(pattern),
|
||||
Class.description_es.ilike(pattern),
|
||||
Class.description_en.ilike(pattern),
|
||||
)
|
||||
)
|
||||
else:
|
||||
if filters.get("class_code"):
|
||||
query = query.filter(
|
||||
Class.class_code.ilike(f"%{filters['class_code']}%")
|
||||
)
|
||||
if filters.get("description"):
|
||||
description_pattern = f"%{filters['description']}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
Class.description_es.ilike(description_pattern),
|
||||
Class.description_en.ilike(description_pattern),
|
||||
)
|
||||
)
|
||||
if filters.get("material_key"):
|
||||
query = query.filter(
|
||||
Class.material_key.ilike(f"%{filters['material_key']}%")
|
||||
|
||||
@@ -58,7 +58,9 @@ export interface A76ClassListParams {
|
||||
page_size?: number;
|
||||
class_code?: string;
|
||||
description?: string;
|
||||
q?: string; // Agregado por si usas búsqueda general
|
||||
/** Búsqueda OR en class_code, description_es y description_en (backend ClassService) */
|
||||
search?: string;
|
||||
q?: string;
|
||||
sort_by?: string;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
@@ -1,156 +1,220 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Search, Loader2, Tag, Ruler } from "lucide-svelte";
|
||||
import { classesApi, type A76Class } from "$lib/api/dashboard/a76/classes"; // Ajusta ruta
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Search, Loader2, Tag, Ruler } from 'lucide-svelte';
|
||||
import { classesApi, type A76Class } from '$lib/api/dashboard/a76/classes';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
// --- PROPS ---
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean,
|
||||
onSelect: (item: A76Class) => void
|
||||
} = $props();
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean;
|
||||
onSelect: (item: A76Class) => void;
|
||||
} = $props();
|
||||
|
||||
// --- ESTADO ---
|
||||
let items = $state<A76Class[]>([]);
|
||||
let loading = $state(false);
|
||||
let searchTerm = $state("");
|
||||
let loaded = $state(false);
|
||||
let items = $state<A76Class[]>([]);
|
||||
let total = $state(0);
|
||||
let currentPage = $state(1);
|
||||
let loading = $state(false);
|
||||
let loadingMore = $state(false);
|
||||
let searchTerm = $state('');
|
||||
|
||||
// Filtro local: Busca por Clave (class_code) o Descripción (description_es)
|
||||
let filteredItems = $derived(
|
||||
items.filter(i =>
|
||||
(i.class_code || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(i.description_es || "").toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
);
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open && !loaded && companyStore.activeCompany?.id) {
|
||||
loadClasses();
|
||||
}
|
||||
});
|
||||
const hasMore = $derived(items.length < total && total > 0);
|
||||
|
||||
async function loadClasses() {
|
||||
if (!companyStore.activeCompany?.id) return;
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
|
||||
const res = await classesApi.list({
|
||||
company_id: companyStore.activeCompany.id,
|
||||
page: 1,
|
||||
page_size: 100
|
||||
});
|
||||
|
||||
|
||||
const data = (res as any).data || res;
|
||||
|
||||
|
||||
const list = data.items || data.classes || [];
|
||||
|
||||
if (Array.isArray(list)) {
|
||||
items = list;
|
||||
loaded = true;
|
||||
} else {
|
||||
console.warn("No encontré lista de clases en la respuesta:", data);
|
||||
}
|
||||
async function loadPage(trimmed: string, page: number, append: boolean) {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error cargando clases:", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
if (append) {
|
||||
if (loadingMore || loading || !hasMore) return;
|
||||
} else if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
function handleSelect(item: A76Class) {
|
||||
if (onSelect) onSelect(item);
|
||||
open = false;
|
||||
}
|
||||
if (page === 1) {
|
||||
loading = true;
|
||||
} else {
|
||||
loadingMore = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await classesApi.list({
|
||||
company_id: companyId,
|
||||
page,
|
||||
page_size: PAGE_SIZE,
|
||||
...(trimmed ? { search: trimmed } : {})
|
||||
});
|
||||
|
||||
const data = (res as { data?: { items?: A76Class[]; total?: number } }).data ?? res;
|
||||
const raw = (data as { items?: A76Class[] }).items ?? [];
|
||||
const newItems = Array.isArray(raw) ? raw : [];
|
||||
|
||||
if (append && newItems.length === 0) {
|
||||
total = items.length;
|
||||
return;
|
||||
}
|
||||
|
||||
const t =
|
||||
typeof (data as { total?: number }).total === 'number'
|
||||
? (data as { total: number }).total
|
||||
: append
|
||||
? items.length + newItems.length
|
||||
: newItems.length;
|
||||
|
||||
if (append) {
|
||||
items = [...items, ...newItems];
|
||||
} else {
|
||||
items = newItems;
|
||||
}
|
||||
total = t;
|
||||
currentPage = page;
|
||||
} catch (e) {
|
||||
console.error('Error cargando clases:', e);
|
||||
if (!append) {
|
||||
items = [];
|
||||
total = 0;
|
||||
}
|
||||
} finally {
|
||||
loading = false;
|
||||
loadingMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll(e: Event) {
|
||||
const target = e.currentTarget as HTMLDivElement;
|
||||
const threshold = 80;
|
||||
if (target.scrollHeight - target.scrollTop - target.clientHeight > threshold) return;
|
||||
if (!hasMore || loading || loadingMore) return;
|
||||
const term = searchTerm.trim();
|
||||
loadPage(term, currentPage + 1, true);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!open) {
|
||||
searchTerm = '';
|
||||
items = [];
|
||||
total = 0;
|
||||
currentPage = 1;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!open) return;
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
|
||||
const term = searchTerm.trim();
|
||||
const delayMs = term === '' ? 0 : 300;
|
||||
const handle = setTimeout(() => {
|
||||
loadPage(term, 1, false);
|
||||
}, delayMs);
|
||||
return () => clearTimeout(handle);
|
||||
});
|
||||
|
||||
function handleSelect(item: A76Class) {
|
||||
if (onSelect) onSelect(item);
|
||||
open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open={open}>
|
||||
<Dialog.Content class="sm:max-w-[700px] max-h-[80vh] flex flex-col z-[300]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Seleccionar Clase (Anexo 24)</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Seleccione la clasificación del material.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Content class="sm:max-w-[700px] max-h-[80vh] flex flex-col z-[300]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Seleccionar Clase (Anexo 24)</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Seleccione la clasificación del material. Desplace hacia abajo para cargar más resultados.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="relative w-full my-2">
|
||||
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Buscar por Clave o Descripción..."
|
||||
class="pl-9"
|
||||
bind:value={searchTerm}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative w-full my-2">
|
||||
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Buscar por Clave o Descripción..."
|
||||
class="pl-9"
|
||||
bind:value={searchTerm}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto border rounded-md min-h-[300px]">
|
||||
{#if loading}
|
||||
<div class="flex flex-col items-center justify-center h-48 gap-2 text-muted-foreground">
|
||||
<Loader2 class="h-8 w-8 animate-spin text-primary" />
|
||||
<p>Cargando catálogo...</p>
|
||||
</div>
|
||||
{:else if filteredItems.length === 0}
|
||||
<div class="flex flex-col items-center justify-center h-48 text-muted-foreground">
|
||||
<p>No se encontraron clases.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/50 sticky top-0 backdrop-blur-sm">
|
||||
<tr class="text-left border-b">
|
||||
<th class="p-3 font-medium text-muted-foreground w-[100px]">Clave</th>
|
||||
<th class="p-3 font-medium text-muted-foreground">Descripción</th>
|
||||
<th class="p-3 font-medium text-muted-foreground w-[80px]">UM</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredItems as item}
|
||||
<tr
|
||||
class="border-b hover:bg-accent/50 transition-colors cursor-pointer"
|
||||
onclick={() => handleSelect(item)}
|
||||
>
|
||||
<td class="p-3">
|
||||
<span class="font-mono font-bold text-primary bg-primary/10 px-2 py-1 rounded text-xs">
|
||||
{item.class_code}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="p-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag class="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
<span class="truncate max-w-[300px]" title={item.description_es || ''}>
|
||||
{item.description_es || 'Sin descripción'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<div
|
||||
class="flex-1 overflow-y-auto border rounded-md min-h-[300px]"
|
||||
onscroll={handleScroll}
|
||||
>
|
||||
{#if loading}
|
||||
<div class="flex flex-col items-center justify-center h-48 gap-2 text-muted-foreground">
|
||||
<Loader2 class="h-8 w-8 animate-spin text-primary" />
|
||||
<p>Cargando catálogo...</p>
|
||||
</div>
|
||||
{:else if items.length === 0}
|
||||
<div class="flex flex-col items-center justify-center h-48 text-muted-foreground">
|
||||
<p>No se encontraron clases.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/50 sticky top-0 backdrop-blur-sm">
|
||||
<tr class="text-left border-b">
|
||||
<th class="p-3 font-medium text-muted-foreground w-[100px]">Clave</th>
|
||||
<th class="p-3 font-medium text-muted-foreground">Descripción</th>
|
||||
<th class="p-3 font-medium text-muted-foreground w-[80px]">UM</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each items as item}
|
||||
<tr
|
||||
class="border-b hover:bg-accent/50 transition-colors cursor-pointer"
|
||||
onclick={() => handleSelect(item)}
|
||||
>
|
||||
<td class="p-3">
|
||||
<span
|
||||
class="font-mono font-bold text-primary bg-primary/10 px-2 py-1 rounded text-xs"
|
||||
>
|
||||
{item.class_code}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="p-3">
|
||||
<div class="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Ruler class="h-3 w-3" />
|
||||
{item.unit_of_measure || '-'}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
<td class="p-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag class="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
<span
|
||||
class="truncate max-w-[300px]"
|
||||
title={item.description_es || item.description_en || ''}
|
||||
>
|
||||
{item.description_es || item.description_en || 'Sin descripción'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<Dialog.Footer>
|
||||
<div class="text-xs text-muted-foreground self-center mr-auto">
|
||||
{filteredItems.length} registros encontrados
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => open = false}>Cancelar</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<td class="p-3">
|
||||
<div class="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Ruler class="h-3 w-3" />
|
||||
{item.unit_of_measure || '-'}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{#if loadingMore}
|
||||
<div class="flex justify-center py-3 text-muted-foreground">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<div class="text-xs text-muted-foreground self-center mr-auto">
|
||||
{#if loading}
|
||||
…
|
||||
{:else}
|
||||
Mostrando {items.length} de {total} registros
|
||||
{/if}
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Search, Loader2 } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { classesApi } from '$lib/api/dashboard/a76/classes';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
@@ -16,86 +17,109 @@
|
||||
} = $props();
|
||||
|
||||
let searchQuery = $state('');
|
||||
let isSearching = $state(false);
|
||||
let classes = $state<any[]>([]);
|
||||
let displayedClasses = $state<any[]>([]);
|
||||
let total = $state(0);
|
||||
let currentPage = $state(1);
|
||||
let itemsPerPage = 10;
|
||||
let loading = $state(false);
|
||||
let loadingMore = $state(false);
|
||||
|
||||
const filteredClasses = $derived(
|
||||
searchQuery
|
||||
? classes.filter(
|
||||
(c) =>
|
||||
c.class_code?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
c.description_es?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: classes
|
||||
);
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
searchClasses();
|
||||
}
|
||||
});
|
||||
const hasMore = $derived(classes.length < total && total > 0);
|
||||
|
||||
$effect(() => {
|
||||
currentPage = 1;
|
||||
loadMoreClasses();
|
||||
});
|
||||
|
||||
async function searchClasses() {
|
||||
async function loadPage(trimmed: string, page: number, append: boolean) {
|
||||
const activeCompanyId = companyStore?.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
toast.error('No hay compañía activa');
|
||||
return;
|
||||
}
|
||||
|
||||
isSearching = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api-sveltekit/classes?company_id=${activeCompanyId}&limit=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
if (append) {
|
||||
if (loadingMore || loading || !hasMore) return;
|
||||
} else if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Error al buscar clases');
|
||||
if (page === 1) {
|
||||
loading = true;
|
||||
} else {
|
||||
loadingMore = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await classesApi.list({
|
||||
company_id: activeCompanyId,
|
||||
page,
|
||||
page_size: PAGE_SIZE,
|
||||
...(trimmed ? { search: trimmed } : {})
|
||||
});
|
||||
const data = (res as { data?: { items?: any[]; total?: number } }).data ?? res;
|
||||
const raw = (data as { items?: any[] }).items ?? [];
|
||||
const newItems = Array.isArray(raw) ? raw : [];
|
||||
|
||||
if (append && newItems.length === 0) {
|
||||
total = classes.length;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
classes = data.items || [];
|
||||
loadMoreClasses();
|
||||
const t =
|
||||
typeof (data as { total?: number }).total === 'number'
|
||||
? (data as { total: number }).total
|
||||
: append
|
||||
? classes.length + newItems.length
|
||||
: newItems.length;
|
||||
|
||||
if (append) {
|
||||
classes = [...classes, ...newItems];
|
||||
} else {
|
||||
classes = newItems;
|
||||
}
|
||||
total = t;
|
||||
currentPage = page;
|
||||
} catch (error) {
|
||||
console.error('Error searching classes:', error);
|
||||
toast.error('Error al buscar clases');
|
||||
classes = [];
|
||||
if (!append) {
|
||||
classes = [];
|
||||
total = 0;
|
||||
}
|
||||
} finally {
|
||||
isSearching = false;
|
||||
loading = false;
|
||||
loadingMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadMoreClasses() {
|
||||
const start = 0;
|
||||
const end = currentPage * itemsPerPage;
|
||||
displayedClasses = filteredClasses.slice(start, end);
|
||||
}
|
||||
|
||||
function handleScroll(e: Event) {
|
||||
const target = e.target as HTMLDivElement;
|
||||
const threshold = 100;
|
||||
const scrolledToBottom =
|
||||
target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
|
||||
|
||||
if (scrolledToBottom && displayedClasses.length < filteredClasses.length) {
|
||||
currentPage++;
|
||||
loadMoreClasses();
|
||||
}
|
||||
const target = e.currentTarget as HTMLDivElement;
|
||||
const threshold = 80;
|
||||
if (target.scrollHeight - target.scrollTop - target.clientHeight > threshold) return;
|
||||
if (!hasMore || loading || loadingMore) return;
|
||||
const term = searchQuery.trim();
|
||||
loadPage(term, currentPage + 1, true);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!open) {
|
||||
searchQuery = '';
|
||||
classes = [];
|
||||
total = 0;
|
||||
currentPage = 1;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!open) return;
|
||||
const activeCompanyId = companyStore?.activeCompany?.id;
|
||||
if (!activeCompanyId) return;
|
||||
|
||||
const term = searchQuery.trim();
|
||||
const delayMs = term === '' ? 0 : 300;
|
||||
const handle = setTimeout(() => {
|
||||
loadPage(term, 1, false);
|
||||
}, delayMs);
|
||||
return () => clearTimeout(handle);
|
||||
});
|
||||
|
||||
function handleSelect(classItem: any) {
|
||||
if (onSelect) {
|
||||
onSelect(classItem);
|
||||
@@ -108,7 +132,9 @@
|
||||
<Dialog.Content class="max-w-4xl max-h-[80vh] flex flex-col">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Seleccionar Clase</Dialog.Title>
|
||||
<Dialog.Description>Busca y selecciona una clase para la partida</Dialog.Description>
|
||||
<Dialog.Description>
|
||||
Busca y selecciona una clase para la partida. Desplázate hacia abajo para más resultados.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="flex gap-2 mb-4">
|
||||
@@ -122,8 +148,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto border rounded-md" onscroll={handleScroll}>
|
||||
{#if isSearching}
|
||||
<div
|
||||
class="flex-1 overflow-auto border rounded-md max-h-[55vh]"
|
||||
onscroll={handleScroll}
|
||||
>
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<Loader2 class="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
@@ -138,14 +167,14 @@
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if displayedClasses.length === 0}
|
||||
{#if classes.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={4} class="text-center py-8 text-muted-foreground">
|
||||
No se encontraron clases
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each displayedClasses as classItem}
|
||||
{#each classes as classItem}
|
||||
<Table.Row
|
||||
class="cursor-pointer hover:bg-muted/50"
|
||||
onclick={() => handleSelect(classItem)}
|
||||
@@ -165,11 +194,19 @@
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
{#if loadingMore}
|
||||
<div class="flex justify-center py-3">
|
||||
<Loader2 class="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
|
||||
<Dialog.Footer class="flex flex-wrap items-center gap-2 justify-between">
|
||||
{#if !loading && classes.length > 0}
|
||||
<span class="text-xs text-muted-foreground">Mostrando {classes.length} de {total}</span>
|
||||
{/if}
|
||||
<Button variant="outline" class="ml-auto" onclick={() => (open = false)}>Cancelar</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
Reference in New Issue
Block a user