Merge pull request 'fix/doda' (#129) from fix/doda into development
Reviewed-on: ADUANASOFT/anexo76#129
This commit is contained in:
@@ -20,12 +20,14 @@
|
||||
let loaded = $state(false);
|
||||
|
||||
let filteredItems = $derived(
|
||||
items.filter(
|
||||
(i) =>
|
||||
i.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
i.broker_key?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
i.license?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
items.filter((i) => {
|
||||
const search = searchTerm.toLowerCase();
|
||||
return (
|
||||
(i.name?.toLowerCase() || '').includes(search) ||
|
||||
(i.broker_key?.toLowerCase() || '').includes(search) ||
|
||||
(i.license?.toLowerCase() || '').includes(search)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
@@ -38,10 +40,15 @@
|
||||
if (!companyStore.activeCompany?.id) return;
|
||||
loading = true;
|
||||
try {
|
||||
const res = await customsBrokersApi.list(companyStore.activeCompany.id.toString());
|
||||
if (res.data?.items) {
|
||||
items = res.data.items;
|
||||
loaded = true;
|
||||
const res = await customsBrokersApi.list(companyStore.activeCompany.id.toString(), 1, 100);
|
||||
const data = (res.data || res) as any;
|
||||
if (data) {
|
||||
if (Array.isArray(data)) {
|
||||
items = data;
|
||||
} else if (data.items && Array.isArray(data.items)) {
|
||||
items = data.items;
|
||||
}
|
||||
loaded = items.length > 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading customs brokers:', e);
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
const search = searchTerm.toLowerCase();
|
||||
return (
|
||||
!searchTerm ||
|
||||
i.description?.toLowerCase().includes(search) ||
|
||||
i.port_code?.toLowerCase().includes(search) ||
|
||||
i.location_description?.toLowerCase().includes(search)
|
||||
(i.description?.toLowerCase() || '').includes(search) ||
|
||||
(i.port_code?.toLowerCase() || '').includes(search) ||
|
||||
(i.location_description?.toLowerCase() || '').includes(search)
|
||||
);
|
||||
})
|
||||
);
|
||||
@@ -47,9 +47,14 @@
|
||||
page: 1,
|
||||
page_size: 100
|
||||
});
|
||||
if (res.data && res.data.items) {
|
||||
items = res.data.items;
|
||||
loaded = true;
|
||||
const data = (res.data || res) as any;
|
||||
if (data) {
|
||||
if (Array.isArray(data)) {
|
||||
items = data;
|
||||
} else if (data.items && Array.isArray(data.items)) {
|
||||
items = data.items;
|
||||
}
|
||||
loaded = items.length > 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading ports:', e);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Search, Loader2, Truck } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { transportersApi, type Transporter } from '$lib/api/dashboard/a76/transporters';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
@@ -20,12 +21,14 @@
|
||||
let loaded = $state(false);
|
||||
|
||||
let filteredItems = $derived(
|
||||
items.filter(
|
||||
(i) =>
|
||||
i.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
i.transporter_key?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
i.rfc?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
items.filter((i) => {
|
||||
const search = searchTerm.toLowerCase();
|
||||
return (
|
||||
(i.name?.toLowerCase() || '').includes(search) ||
|
||||
(i.transporter_key?.toLowerCase() || '').includes(search) ||
|
||||
(i.rfc?.toLowerCase() || '').includes(search)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
@@ -38,14 +41,29 @@
|
||||
if (!companyStore.activeCompany?.id) return;
|
||||
loading = true;
|
||||
try {
|
||||
const res = await transportersApi.list(companyStore.activeCompany.id);
|
||||
// Explicitly set pagination to avoid defaults
|
||||
const res = await transportersApi.list(companyStore.activeCompany.id, {
|
||||
page: 1,
|
||||
page_size: 100
|
||||
});
|
||||
const data = (res as any).data || res;
|
||||
if (data && data.items) {
|
||||
items = data.items;
|
||||
if (data) {
|
||||
if (Array.isArray(data)) {
|
||||
items = data;
|
||||
} else if (data.items) {
|
||||
// Handle case where items might be an array or inside an object
|
||||
items = Array.isArray(data.items) ? data.items : [];
|
||||
}
|
||||
loaded = true;
|
||||
} else if ((res as any).error) {
|
||||
console.error('API Error loading transporters:', (res as any).error);
|
||||
toast.error('Error al cargar transportistas: ' + (res as any).error);
|
||||
loaded = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading transporters:', e);
|
||||
toast.error('Error de conexión al cargar transportistas');
|
||||
loaded = true;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Plus, Pencil, Trash2 } from 'lucide-svelte';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
interface Column {
|
||||
header: string;
|
||||
key: string;
|
||||
render?: (value: any) => string | any;
|
||||
}
|
||||
|
||||
let {
|
||||
title = '',
|
||||
columns = [],
|
||||
data = [],
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
class: className = ''
|
||||
}: {
|
||||
title?: string;
|
||||
columns: Column[];
|
||||
data: any[];
|
||||
onAdd?: () => void;
|
||||
onEdit?: (item: any, index: number) => void;
|
||||
onDelete?: (item: any, index: number) => void;
|
||||
class?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class={cn('space-y-4 rounded-xl border bg-card p-4 shadow-sm', className)}>
|
||||
<div class="flex items-center justify-between">
|
||||
{#if title}
|
||||
<h3 class="text-sm font-semibold tracking-wider text-muted-foreground uppercase">{title}</h3>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="relative overflow-hidden rounded-md border bg-background">
|
||||
<Table.Root>
|
||||
<Table.Header class="bg-muted/50">
|
||||
<Table.Row>
|
||||
{#each columns as col}
|
||||
<Table.Head class="h-10 px-4 text-xs font-semibold whitespace-nowrap"
|
||||
>{col.header}</Table.Head
|
||||
>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if data.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell
|
||||
colspan={columns.length}
|
||||
class="h-24 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
No hay registros.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each data as row, i}
|
||||
<Table.Row class="group transition-colors hover:bg-muted/30">
|
||||
{#each columns as col}
|
||||
<Table.Cell class="px-4 py-2 text-sm whitespace-nowrap">
|
||||
{#if col.render}
|
||||
{col.render(row[col.key])}
|
||||
{:else}
|
||||
{row[col.key] ?? '-'}
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" onclick={onAdd} class="h-8 gap-1 px-3 text-xs font-medium">
|
||||
<Plus class="h-3.5 w-3.5" />
|
||||
Nuevo
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onclick={() => {}}
|
||||
class="h-8 gap-1 px-3 text-xs font-medium"
|
||||
disabled={data.length === 0}
|
||||
>
|
||||
<Pencil class="h-3.5 w-3.5" />
|
||||
Editar
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onclick={() => {}}
|
||||
class="h-8 gap-1 px-3 text-xs font-medium"
|
||||
disabled={data.length === 0}
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
Borrar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +1,57 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import { renderComponent, renderSnippet } from '$lib/components/ui/data-table';
|
||||
import { createRawSnippet } from 'svelte';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
function formatDate(date?: string | null): string {
|
||||
if (!date) return '-';
|
||||
// Supposing created_at is an ISO string or similar
|
||||
try {
|
||||
return new Date(date).toLocaleDateString('es-MX', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Doda>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'integration_number',
|
||||
header: 'No. Integración',
|
||||
cell: ({ row }) => row.original.integration_number || 'N/A'
|
||||
accessorKey: 'id',
|
||||
header: 'Folio',
|
||||
cell: ({ row }) => {
|
||||
const numberSnippet = createRawSnippet<[{ number: number }]>((getProps) => {
|
||||
const { number } = getProps();
|
||||
return {
|
||||
render: () =>
|
||||
`<code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">${number}</code>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(numberSnippet, { number: row.original.id });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Fecha doda',
|
||||
cell: ({ row }) => {
|
||||
const dateSnippet = createRawSnippet<[{ date: string }]>((getProps) => {
|
||||
const { date } = getProps();
|
||||
return {
|
||||
render: () =>
|
||||
`<div class="text-sm text-muted-foreground">${date}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(dateSnippet, { date: formatDate(row.original.created_at) });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'dispatch_customs',
|
||||
header: 'Desp',
|
||||
cell: ({ row }) => row.original.dispatch_customs || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'patent',
|
||||
@@ -17,18 +60,65 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Doda>[] {
|
||||
},
|
||||
{
|
||||
accessorKey: 'pedimentos',
|
||||
header: 'Pedimentos',
|
||||
header: 'Pedimento(s)',
|
||||
cell: ({ row }) => row.original.pedimentos || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'doda_date',
|
||||
header: 'Fecha',
|
||||
cell: ({ row }) => row.original.doda_date || 'N/A'
|
||||
accessorKey: 'shipments',
|
||||
header: 'Remesa(s)',
|
||||
cell: ({ row }) => row.original.shipments || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'integration_number',
|
||||
header: 'Integracion',
|
||||
cell: ({ row }) => {
|
||||
const numberSnippet = createRawSnippet<[{ number?: string | null }]>((getProps) => {
|
||||
const { number } = getProps();
|
||||
return {
|
||||
render: () =>
|
||||
`<code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">${number || 'N/A'}</code>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(numberSnippet, { number: row.original.integration_number });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'transaction_number',
|
||||
header: 'No transaccion',
|
||||
cell: ({ row }) => row.original.transaction_number || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'transport_identification',
|
||||
header: 'Id transporte',
|
||||
cell: ({ row }) => row.original.transport_identification || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'caat',
|
||||
header: 'CAAT',
|
||||
cell: ({ row }) => row.original.caat || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'last_user',
|
||||
header: 'Usuario',
|
||||
cell: ({ row }) => row.original.last_user || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Estatus',
|
||||
cell: ({ row }) => row.original.status || 'N/A'
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const statusSnippet = createRawSnippet<[{ status?: string | null }]>((getProps) => {
|
||||
const { status } = getProps();
|
||||
const colorClass = status === 'VALIDADO' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
|
||||
return {
|
||||
render: () =>
|
||||
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">
|
||||
${status || '-'}
|
||||
</span>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(statusSnippet, { status });
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import { type ColumnDef, getCoreRowModel, type RowSelectionState } from '@tanstack/table-core';
|
||||
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';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
pageCount: number;
|
||||
totalItems: number;
|
||||
loading: boolean;
|
||||
hasMore: boolean;
|
||||
loadMore: () => void;
|
||||
selectedId?: number | null;
|
||||
onRowClick?: (row: TData) => void;
|
||||
};
|
||||
@@ -18,8 +17,9 @@
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
pageCount,
|
||||
totalItems,
|
||||
loading,
|
||||
hasMore,
|
||||
loadMore,
|
||||
selectedId = null,
|
||||
onRowClick
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
@@ -37,26 +37,44 @@
|
||||
}
|
||||
},
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: false,
|
||||
manualPagination: true,
|
||||
pageCount: pageCount
|
||||
enableMultiRowSelection: false
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url);
|
||||
}
|
||||
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="space-y-4">
|
||||
<div class="rounded-md border">
|
||||
<div class="w-full">
|
||||
<div class="max-h-[600px] overflow-y-auto rounded-md border" bind:this={scrollContainer}>
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Header class="bg-background">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head class="whitespace-nowrap">
|
||||
<Table.Head>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
@@ -73,12 +91,12 @@
|
||||
<Table.Row
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
onclick={() => onRowClick?.(row.original)}
|
||||
class="cursor-pointer hover:bg-muted/50 transition-colors {row.getIsSelected()
|
||||
? 'bg-primary/10'
|
||||
: ''}"
|
||||
class="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">
|
||||
<Table.Cell>
|
||||
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
@@ -90,32 +108,26 @@
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
|
||||
{#if hasMore}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-20 text-center">
|
||||
<div bind:this={loadingTrigger}>
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<div
|
||||
class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"
|
||||
></div>
|
||||
<span class="text-sm text-muted-foreground">Cargando más...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-muted-foreground">Desplázate para cargar más</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<!-- Paginación -->
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<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, MapPin } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import {
|
||||
customsSectionsApi,
|
||||
type CustomsSection
|
||||
} from '$lib/api/dashboard/reference_data/customs_sections';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean;
|
||||
onSelect: (item: CustomsSection) => void;
|
||||
} = $props();
|
||||
|
||||
let items = $state<CustomsSection[]>([]);
|
||||
let loading = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let loaded = $state(false);
|
||||
|
||||
let filteredItems = $derived(
|
||||
items.filter((i) => {
|
||||
const search = searchTerm.toLowerCase();
|
||||
return (
|
||||
!searchTerm ||
|
||||
(i.section_name?.toLowerCase() || '').includes(search) ||
|
||||
(i.customs_code?.toLowerCase() || '').includes(search)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (open && !loaded) {
|
||||
loadItems();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadItems() {
|
||||
loading = true;
|
||||
try {
|
||||
// Customs sections are public reference data
|
||||
// Reduced page_size to 100 to comply with backend constraints (le=100)
|
||||
const res = await customsSectionsApi.list(1, 100);
|
||||
const data = (res.data || res) as any;
|
||||
if (data) {
|
||||
// Handle both direct array and object-with-items wrapper
|
||||
items = Array.isArray(data) ? data : data.items || [];
|
||||
loaded = true;
|
||||
} else if (res.error) {
|
||||
console.error('API Error loading customs sections:', res.error);
|
||||
toast.error('Error al cargar secciones: ' + res.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading customs sections:', e);
|
||||
toast.error('Error de conexión al cargar las secciones');
|
||||
// Even on error, mark as loaded to prevent infinite loops, or handle with a retry button
|
||||
loaded = true;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(item: CustomsSection) {
|
||||
if (onSelect) onSelect(item);
|
||||
open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="flex max-h-[80vh] flex-col sm:max-w-[700px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Seleccionar Sección Aduanera</Dialog.Title>
|
||||
<Dialog.Description>Catálogo general de aduanas y secciones.</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="relative my-2 w-full">
|
||||
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Buscar por descripción o código..."
|
||||
class="pl-9"
|
||||
bind:value={searchTerm}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[300px] flex-1 overflow-y-auto rounded-md border">
|
||||
{#if loading}
|
||||
<div class="flex h-48 flex-col items-center justify-center 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 h-48 flex-col items-center justify-center text-muted-foreground">
|
||||
<p>No se encontraron registros.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/50">
|
||||
<tr class="border-b text-left">
|
||||
<th class="w-[100px] p-3 font-medium text-muted-foreground">Código</th>
|
||||
<th class="p-3 font-medium text-muted-foreground">Descripción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredItems as item}
|
||||
<tr
|
||||
class="cursor-pointer border-b transition-colors hover:bg-accent/50"
|
||||
onclick={() => handleSelect(item)}
|
||||
>
|
||||
<td class="p-3 font-mono font-bold text-primary">{item.customs_code}</td>
|
||||
<td class="p-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<MapPin class="h-3 w-3 text-muted-foreground" />
|
||||
{item.section_name || '-'}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<div class="mr-auto self-center text-xs text-muted-foreground">
|
||||
Mostrando {filteredItems.length} registros
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
Reference in New Issue
Block a user