feat: Implement Ventanilla Única (VU) management for customs brokers, including API, UI, and data model updates, and add a new state selection dialog.
This commit is contained in:
@@ -28,8 +28,12 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBroker>[
|
||||
cell: ({ row }) => {
|
||||
const typeSnippet = createRawSnippet<[{ type: string | null | undefined }]>((getType) => {
|
||||
const { type } = getType();
|
||||
let display = type || '-';
|
||||
if (type === 'MEX') display = 'Agente Aduanal Mexicano';
|
||||
else if (type === 'USA') display = 'Agente Aduanal Americano (Broker)';
|
||||
|
||||
return {
|
||||
render: () => `<div class="max-w-[120px] truncate">${type || '-'}</div>`
|
||||
render: () => `<div class="max-w-[200px] truncate text-sm">${display}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(typeSnippet, { type: row.original.type });
|
||||
@@ -68,8 +72,8 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBroker>[
|
||||
const postalSnippet = createRawSnippet<[{ postal: string | null | undefined }]>((getPostal) => {
|
||||
const { postal } = getPostal();
|
||||
return {
|
||||
render: () =>
|
||||
postal
|
||||
render: () =>
|
||||
postal
|
||||
? `<code class="bg-muted px-1 py-0.5 rounded text-sm">${postal}</code>`
|
||||
: `<span class="text-muted-foreground">-</span>`
|
||||
};
|
||||
@@ -136,8 +140,8 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBroker>[
|
||||
const licenseSnippet = createRawSnippet<[{ license: string | null | undefined }]>((getLicense) => {
|
||||
const { license } = getLicense();
|
||||
return {
|
||||
render: () =>
|
||||
license
|
||||
render: () =>
|
||||
license
|
||||
? `<code class="relative rounded bg-blue-100 dark:bg-blue-900 px-[0.3rem] py-[0.2rem] font-mono text-xs font-semibold">${license}</code>`
|
||||
: `<span class="text-muted-foreground">-</span>`
|
||||
};
|
||||
@@ -175,9 +179,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBroker>[
|
||||
id: "actions",
|
||||
header: "Acciones",
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
return renderComponent(DataTableActions, {
|
||||
broker: row.original,
|
||||
onSuccess
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Loader2, Search } from 'lucide-svelte';
|
||||
import { statesApi, type State } from '$lib/api/dashboard/reference_data/states';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
onSelect,
|
||||
countryCode = ''
|
||||
}: {
|
||||
open: boolean;
|
||||
onSelect: (state: State) => void;
|
||||
countryCode?: string;
|
||||
} = $props();
|
||||
|
||||
let items: State[] = $state([]);
|
||||
let filteredItems: State[] = $state([]);
|
||||
let loading = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let error = $state('');
|
||||
|
||||
async function loadStates() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const response = await statesApi.list(1, 100);
|
||||
if (response.data?.items) {
|
||||
items = response.data.items;
|
||||
filteredItems = items;
|
||||
} else if (response.error) {
|
||||
error = response.error;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error loading states';
|
||||
console.error('Error loading states:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function filterStates() {
|
||||
let list = items;
|
||||
|
||||
// Filter by country if provided
|
||||
if (countryCode) {
|
||||
list = list.filter((item) => item.m3_key === countryCode);
|
||||
}
|
||||
|
||||
// Filter by search term
|
||||
if (searchTerm.trim()) {
|
||||
const term = searchTerm.toLowerCase();
|
||||
list = list.filter(
|
||||
(item) =>
|
||||
item.m3_key?.toLowerCase().includes(term) ||
|
||||
item.mex_key?.toLowerCase().includes(term) ||
|
||||
item.description?.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
|
||||
filteredItems = list;
|
||||
}
|
||||
|
||||
function handleSelect(item: State) {
|
||||
onSelect(item);
|
||||
open = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
loadStates();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
filterStates();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="flex max-h-[90vh] w-[70vw] !max-w-[70vw] flex-col p-0">
|
||||
<Dialog.Header class="border-b px-6 py-4">
|
||||
<Dialog.Title class="text-lg font-semibold">CATALOGO DE ESTADOS</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="border-b bg-zinc-50 px-6 py-3 dark:bg-zinc-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="h-4 w-4 text-zinc-400" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscando por clave o nombre..."
|
||||
class="h-9 flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto px-6 py-4">
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-20">
|
||||
<Loader2 class="h-8 w-8 animate-spin text-zinc-900 dark:text-zinc-100" />
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="flex items-center justify-center py-20 text-red-600">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-hidden rounded-md border">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-zinc-900 text-white dark:bg-zinc-800">
|
||||
<tr>
|
||||
<th class="w-24 border-r border-zinc-700 px-3 py-2 text-left font-semibold"
|
||||
>Clave M3</th
|
||||
>
|
||||
<th class="w-24 border-r border-zinc-700 px-3 py-2 text-left font-semibold"
|
||||
>Clave Mex</th
|
||||
>
|
||||
<th class="w-24 border-r border-zinc-700 px-3 py-2 text-left font-semibold"
|
||||
>Clave Ame</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold">Descripción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredItems as item}
|
||||
<tr
|
||||
class="cursor-pointer border-b transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
||||
onclick={() => handleSelect(item)}
|
||||
>
|
||||
<td class="border-r px-3 py-2 font-mono font-semibold">{item.m3_key || ''}</td>
|
||||
<td class="border-r px-3 py-2">{item.mex_key || ''}</td>
|
||||
<td class="border-r px-3 py-2">{item.ame_key || ''}</td>
|
||||
<td class="px-3 py-2">{item.description || ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if filteredItems.length === 0}
|
||||
<tr>
|
||||
<td colspan="4" class="px-3 py-8 text-center text-zinc-500">
|
||||
No se encontraron resultados
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-2 border-t bg-zinc-50 px-6 py-3 dark:bg-zinc-900">
|
||||
<Button variant="outline" size="sm" onclick={() => (open = false)}>Cancelar</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
Reference in New Issue
Block a user