feature/partidas-pais-origen
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Loader2, Search } from 'lucide-svelte';
|
||||
import { countriesApi } from '$lib/api/dashboard/reference_data/countries';
|
||||
import { countriesApi, type Country } from '$lib/api/dashboard/reference_data/countries';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
@@ -12,17 +12,43 @@
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean;
|
||||
onSelect: (country: any) => void;
|
||||
onSelect: (country: Country) => void;
|
||||
} = $props();
|
||||
|
||||
let countries: any[] = $state([]);
|
||||
let filteredCountries: any[] = $state([]);
|
||||
let countries = $state<Country[]>([]);
|
||||
let loading = $state(false);
|
||||
let loadingMore = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let previousSearchTerm = '';
|
||||
let page = $state(1);
|
||||
const pageSize = 100;
|
||||
let hasMore = $state(true);
|
||||
let totalItems = $state(0);
|
||||
let error = $state('');
|
||||
let observer: IntersectionObserver | null = null;
|
||||
let bottomSentinel: HTMLElement | null = $state(null);
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
let isInitialized = false;
|
||||
|
||||
async function loadCountries() {
|
||||
loading = true;
|
||||
async function resetAndLoad() {
|
||||
page = 1;
|
||||
countries = [];
|
||||
hasMore = true;
|
||||
await loadCountries(true);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (!hasMore || loading || loadingMore) return;
|
||||
page += 1;
|
||||
await loadCountries(false);
|
||||
}
|
||||
|
||||
async function loadCountries(isInitial: boolean) {
|
||||
if (isInitial) {
|
||||
loading = true;
|
||||
} else {
|
||||
loadingMore = true;
|
||||
}
|
||||
error = '';
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
@@ -31,13 +57,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Para este diálogo, cargamos una cantidad grande o implementamos paginación si fuera necesario
|
||||
// Por ahora seguimos el patrón original de cargar "todos" (limite 100 en backend)
|
||||
const response = await countriesApi.list(companyId, 1, 100);
|
||||
|
||||
const response = await countriesApi.list(companyId, page, pageSize, searchTerm);
|
||||
|
||||
if (response.data) {
|
||||
countries = response.data.items || [];
|
||||
filteredCountries = countries;
|
||||
const newItems = response.data.items || [];
|
||||
totalItems = response.data.total || 0;
|
||||
if (isInitial) {
|
||||
countries = newItems;
|
||||
} else {
|
||||
countries = [...countries, ...newItems];
|
||||
}
|
||||
hasMore = countries.length < totalItems && newItems.length > 0;
|
||||
} else if (response.error) {
|
||||
error = `Error: ${response.error}`;
|
||||
toast.error(error);
|
||||
@@ -48,30 +78,16 @@
|
||||
toast.error(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
loadingMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
function filterCountries() {
|
||||
if (!searchTerm.trim()) {
|
||||
filteredCountries = countries;
|
||||
} else {
|
||||
const term = searchTerm.toLowerCase();
|
||||
filteredCountries = countries.filter(
|
||||
(country) =>
|
||||
country.m3_key?.toLowerCase().includes(term) ||
|
||||
country.mex_key?.toLowerCase().includes(term) ||
|
||||
country.description_es?.toLowerCase().includes(term) ||
|
||||
country.description_en?.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(country: any) {
|
||||
function handleSelect(country: Country) {
|
||||
onSelect(country);
|
||||
open = false;
|
||||
}
|
||||
|
||||
function handleRowKeydown(event: KeyboardEvent, country: any) {
|
||||
function handleRowKeydown(event: KeyboardEvent, country: Country) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handleSelect(country);
|
||||
@@ -79,13 +95,45 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
loadCountries();
|
||||
if (open && !isInitialized) {
|
||||
isInitialized = true;
|
||||
previousSearchTerm = searchTerm;
|
||||
void resetAndLoad();
|
||||
} else if (!open) {
|
||||
isInitialized = false;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
filterCountries();
|
||||
const term = searchTerm;
|
||||
if (isInitialized && term !== previousSearchTerm) {
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
previousSearchTerm = term;
|
||||
void resetAndLoad();
|
||||
}, 400);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (bottomSentinel && hasMore && !loading && !loadingMore && open) {
|
||||
if (observer) observer.disconnect();
|
||||
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0]?.isIntersecting && hasMore && !loading && !loadingMore) {
|
||||
void loadMore();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
observer.observe(bottomSentinel);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (observer) observer.disconnect();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -100,14 +148,19 @@
|
||||
<Search class="w-4 h-4 text-zinc-400" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscando..."
|
||||
placeholder="Buscar por clave M3, MEX, AME o descripción..."
|
||||
class="flex-1 h-9"
|
||||
type="search"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-zinc-500">
|
||||
{totalItems > 0 ? `${countries.length} de ${totalItems} mostrados` : ''} — desplázate para cargar
|
||||
más; el catálogo completo supera 100 países.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto px-6 py-4">
|
||||
{#if loading}
|
||||
{#if loading && countries.length === 0}
|
||||
<div class="flex items-center justify-center py-20">
|
||||
<Loader2 class="w-8 h-8 animate-spin text-zinc-900 dark:text-zinc-100" />
|
||||
</div>
|
||||
@@ -136,21 +189,21 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredCountries as country}
|
||||
{#each countries as country}
|
||||
<tr
|
||||
class="border-b hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
|
||||
tabindex="0"
|
||||
onclick={() => handleSelect(country)}
|
||||
onkeydown={(event) => handleRowKeydown(event, country)}
|
||||
>
|
||||
<td class="px-3 py-2 border-r">{country.m3_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.mex_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.description_es || ''}</td>
|
||||
<td class="px-3 py-2 border-r text-center">{country.ame_key || ''}</td>
|
||||
<td class="px-3 py-2">{country.description_en || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.m3_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.mex_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.description_es || ''}</td>
|
||||
<td class="px-3 py-2 border-r text-center">{country.ame_key || ''}</td>
|
||||
<td class="px-3 py-2">{country.description_en || ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if filteredCountries.length === 0}
|
||||
{#if countries.length === 0}
|
||||
<tr>
|
||||
<td colspan="5" class="px-3 py-8 text-center text-zinc-500">
|
||||
No se encontraron resultados
|
||||
@@ -161,6 +214,16 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
bind:this={bottomSentinel}
|
||||
class="h-10 flex items-center justify-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{#if loadingMore}
|
||||
<Loader2 class="h-5 w-5 animate-spin text-zinc-500" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Usa flechas para navegar filas y Enter/Espacio para seleccionar.
|
||||
</div>
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
|
||||
function handleCountrySelect(country: any) {
|
||||
customs.origin_country = country.m3_key || country.mex_key;
|
||||
(customs as any).origin_country_name = country.description || country.description_en;
|
||||
(customs as any).origin_country_name = country.description_es || country.description_en;
|
||||
}
|
||||
|
||||
function handleFractionSelect(fraction: any) {
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
if (!editingItem.customs) editingItem.customs = {} as any;
|
||||
if (editingItem.customs) {
|
||||
editingItem.customs.origin_country = country.m3_key || country.mex_key;
|
||||
(editingItem.customs as any).origin_country_name = country.description || country.description_en;
|
||||
(editingItem.customs as any).origin_country_name = country.description_es || country.description_en;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user