Merge origin/development into fix/bug-pedimento
This commit is contained in:
@@ -22,10 +22,29 @@
|
||||
FileText,
|
||||
Settings,
|
||||
User,
|
||||
Trash2
|
||||
Trash2,
|
||||
Search,
|
||||
Globe,
|
||||
MapPin as MapPinIcon,
|
||||
Factory,
|
||||
Calendar,
|
||||
Hash,
|
||||
ShieldCheck,
|
||||
Award,
|
||||
Fingerprint,
|
||||
Briefcase
|
||||
} from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
// Componentes Compartidos (Modales)
|
||||
import CountrySelectorDialog from '$lib/components/dashboard/goods/modales/country-selector-dialog.svelte';
|
||||
import StateSelectorDialog from '$lib/components/dashboard/shared/modals/state-selector-dialog.svelte';
|
||||
import SectorSelectorDialog from '$lib/components/dashboard/shared/modals/sector-selector-dialog.svelte';
|
||||
|
||||
import { type Country } from '$lib/api/dashboard/reference_data/countries';
|
||||
import { type State } from '$lib/api/dashboard/reference_data/states';
|
||||
import { type Sector } from '$lib/api/dashboard/reference_data/sectors';
|
||||
|
||||
// API & Stores
|
||||
import { clientsProvidersApi } from '$lib/api/dashboard/a76/clients-providers';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
@@ -73,17 +92,30 @@
|
||||
program: '',
|
||||
program_number: '',
|
||||
authorization_date_str: '', // String para input date
|
||||
prosec: 0,
|
||||
prosec: '',
|
||||
manufacturer_id: '',
|
||||
tax_id: '',
|
||||
ctpat_svi: '',
|
||||
is_certified_company: '0'
|
||||
is_certified_company: false
|
||||
});
|
||||
|
||||
let formData = $state(getEmptyForm());
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// --- ESTADO PARA MODALES ---
|
||||
let countryModalOpen = $state(false);
|
||||
let stateModalOpen = $state(false);
|
||||
let sectorModalOpen = $state(false);
|
||||
|
||||
const scaiiPrograms = [
|
||||
{ value: 'IMMEX', label: 'IMMEX' },
|
||||
{ value: 'PROSEC', label: 'PROSEC' },
|
||||
{ value: 'ALTEX', label: 'ALTEX' },
|
||||
{ value: 'ECEX', label: 'ECEX' },
|
||||
{ value: 'DRAWBACK', label: 'DRAWBACK' }
|
||||
];
|
||||
|
||||
// --- UTILIDADES ---
|
||||
const intDateToString = (d?: number | null) =>
|
||||
d
|
||||
@@ -138,11 +170,11 @@
|
||||
program: prog.program || '',
|
||||
program_number: prog.program_number || '',
|
||||
authorization_date_str: intDateToString(prog.secon_auth_date),
|
||||
prosec: prog.prosec || 0,
|
||||
prosec: prog.prosec ? String(prog.prosec) : '',
|
||||
manufacturer_id: prog.manufacturer_id || '',
|
||||
tax_id: prog.tax_id || '',
|
||||
ctpat_svi: prog.ctpat_svi || '',
|
||||
is_certified_company: prog.is_certified_company || '0'
|
||||
is_certified_company: prog.is_certified_company === '1'
|
||||
};
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -202,11 +234,11 @@
|
||||
program: clean(formData.program),
|
||||
program_number: clean(formData.program_number),
|
||||
secon_auth_date: stringDateToInt(formData.authorization_date_str),
|
||||
prosec: Number(formData.prosec) || null,
|
||||
prosec: clean(formData.prosec),
|
||||
manufacturer_id: clean(formData.manufacturer_id),
|
||||
tax_id: clean(formData.tax_id),
|
||||
ctpat_svi: clean(formData.ctpat_svi),
|
||||
is_certified_company: clean(formData.is_certified_company)
|
||||
is_certified_company: formData.is_certified_company ? '1' : '0'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -470,17 +502,44 @@
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="state">Estado</Label>
|
||||
<Input id="state" bind:value={formData.state} maxlength={30} disabled={loading} />
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
id="state"
|
||||
bind:value={formData.state}
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onclick={() => (stateModalOpen = true)}
|
||||
disabled={loading}
|
||||
title="Buscar Estado"
|
||||
>
|
||||
<Search size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="country">País (ISO)</Label>
|
||||
<Input
|
||||
id="country"
|
||||
bind:value={formData.country}
|
||||
placeholder="MEX"
|
||||
maxlength={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
id="country"
|
||||
bind:value={formData.country}
|
||||
placeholder="MEX"
|
||||
maxlength={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onclick={() => (countryModalOpen = true)}
|
||||
disabled={loading}
|
||||
title="Buscar País"
|
||||
>
|
||||
<Globe size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -509,77 +568,160 @@
|
||||
>Información sobre IMMEX, PROSEC y otras certificaciones.</Card.Description
|
||||
>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="program">Programa</Label>
|
||||
<Input
|
||||
id="program"
|
||||
bind:value={formData.program}
|
||||
placeholder="IMMEX"
|
||||
maxlength={7}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Card.Content class="space-y-8">
|
||||
<!-- Section: Promotion Programs -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<Briefcase size={18} />
|
||||
<span>Programas de Fomento</span>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="program_num">Número de Programa</Label>
|
||||
<Input
|
||||
id="program_num"
|
||||
bind:value={formData.program_number}
|
||||
maxlength={40}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Separator />
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="grid gap-2 lg:col-span-1">
|
||||
<Label for="program" class="flex items-center gap-2">
|
||||
<Building2 size={14} class="text-muted-foreground" />
|
||||
Programa
|
||||
</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.program}
|
||||
onValueChange={(v) => (formData.program = v)}
|
||||
disabled={loading}
|
||||
>
|
||||
<Select.Trigger id="program" class="w-full">
|
||||
{formData.program || 'Selecciona un programa'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each scaiiPrograms as prog}
|
||||
<Select.Item value={prog.value} label={prog.label}>
|
||||
{prog.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="program_num" class="flex items-center gap-2">
|
||||
<Hash size={14} class="text-muted-foreground" />
|
||||
Número de Programa
|
||||
</Label>
|
||||
<Input
|
||||
id="program_num"
|
||||
bind:value={formData.program_number}
|
||||
maxlength={40}
|
||||
disabled={loading}
|
||||
placeholder="P. ej. 1234-2024"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="auth_date" class="flex items-center gap-2">
|
||||
<Calendar size={14} class="text-muted-foreground" />
|
||||
Fecha Autorización
|
||||
</Label>
|
||||
<Input
|
||||
id="auth_date"
|
||||
type="date"
|
||||
bind:value={formData.authorization_date_str}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="prosec" class="flex items-center gap-2">
|
||||
<Factory size={14} class="text-muted-foreground" />
|
||||
PROSEC (Sector)
|
||||
</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
id="prosec"
|
||||
bind:value={formData.prosec}
|
||||
disabled={loading}
|
||||
placeholder="Ej: XII, IV"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onclick={() => (sectorModalOpen = true)}
|
||||
disabled={loading}
|
||||
title="Buscar Sector"
|
||||
>
|
||||
<Search size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="grid gap-2">
|
||||
<Label for="auth_date">Fecha Autorización</Label>
|
||||
<Input
|
||||
id="auth_date"
|
||||
type="date"
|
||||
bind:value={formData.authorization_date_str}
|
||||
disabled={loading}
|
||||
/>
|
||||
<!-- Section: Industrial Identification -->
|
||||
<div class="space-y-4 pt-4">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<Fingerprint size={18} />
|
||||
<span>Identificación Industrial</span>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="tax_id">Tax ID (Extranjero)</Label>
|
||||
<Input
|
||||
id="tax_id"
|
||||
bind:value={formData.tax_id}
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="man_id">Manufacturer ID</Label>
|
||||
<Input
|
||||
id="man_id"
|
||||
bind:value={formData.manufacturer_id}
|
||||
maxlength={25}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Separator />
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="tax_id" class="flex items-center gap-2">
|
||||
<Globe size={14} class="text-muted-foreground" />
|
||||
Tax ID (Extranjero)
|
||||
</Label>
|
||||
<Input
|
||||
id="tax_id"
|
||||
bind:value={formData.tax_id}
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
placeholder="Identificador fiscal extranjero"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="man_id" class="flex items-center gap-2">
|
||||
<FileText size={14} class="text-muted-foreground" />
|
||||
Manufacturer ID (MID)
|
||||
</Label>
|
||||
<Input
|
||||
id="man_id"
|
||||
bind:value={formData.manufacturer_id}
|
||||
maxlength={25}
|
||||
disabled={loading}
|
||||
placeholder="P. ej. MXABCD12345"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="ctpat">C-TPAT / SVI</Label>
|
||||
<Input
|
||||
id="ctpat"
|
||||
bind:value={formData.ctpat_svi}
|
||||
maxlength={100}
|
||||
disabled={loading}
|
||||
/>
|
||||
<!-- Section: Certifications & Security -->
|
||||
<div class="space-y-4 pt-4">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<ShieldCheck size={18} />
|
||||
<span>Certificaciones y Seguridad</span>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="prosec">PROSEC (Sector)</Label>
|
||||
<Input
|
||||
id="prosec"
|
||||
type="number"
|
||||
bind:value={formData.prosec}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Separator />
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="ctpat" class="flex items-center gap-2">
|
||||
<Award size={14} class="text-muted-foreground" />
|
||||
C-TPAT / SVI
|
||||
</Label>
|
||||
<Input
|
||||
id="ctpat"
|
||||
bind:value={formData.ctpat_svi}
|
||||
maxlength={100}
|
||||
disabled={loading}
|
||||
placeholder="P. ej. SVI-12345"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 rounded-lg border bg-card/50 p-4">
|
||||
<Switch
|
||||
id="is_certified"
|
||||
bind:checked={formData.is_certified_company}
|
||||
disabled={loading}
|
||||
/>
|
||||
<div class="grid gap-0.5">
|
||||
<Label for="is_certified">Empresa Certificada</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Indica si cuenta con certificación de empresa
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
@@ -670,3 +812,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODALES DE SELECCIÓN -->
|
||||
<CountrySelectorDialog
|
||||
bind:open={countryModalOpen}
|
||||
onSelect={(country) => (formData.country = country.m3_key)}
|
||||
/>
|
||||
|
||||
<StateSelectorDialog
|
||||
bind:open={stateModalOpen}
|
||||
onSelect={(state) => {
|
||||
formData.state = state.description;
|
||||
if (state.m3_key && !formData.country) {
|
||||
formData.country = state.m3_key;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<SectorSelectorDialog
|
||||
bind:open={sectorModalOpen}
|
||||
onSelect={(sector) => (formData.prosec = sector.key)}
|
||||
/>
|
||||
|
||||
@@ -187,8 +187,7 @@
|
||||
const brokerColumns = createBrokerColumns(handleActionSuccess);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-[calc(100vh-4rem)] p-4 gap-4 pb-15">
|
||||
<!-- Title -->
|
||||
<div class="flex h-[calc(100vh-4rem)] flex-col gap-4 p-4 pb-15">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h1 class="text-2xl font-bold">GESTIÓN ADUANAL</h1>
|
||||
@@ -196,17 +195,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs.Root bind:value={activeTab} class="flex-1 flex flex-col overflow-hidden">
|
||||
<Tabs.List class="w-full justify-start border-b rounded-none bg-transparent p-0 mb-4">
|
||||
<Tabs.Root bind:value={activeTab} class="flex flex-1 flex-col overflow-hidden">
|
||||
<Tabs.List class="mb-4 w-full justify-start rounded-none border-b bg-transparent p-0">
|
||||
<Tabs.Trigger
|
||||
value="brokers"
|
||||
class="data-[state=active]:border-primary border-b-2 border-transparent rounded-none"
|
||||
class="rounded-none border-b-2 border-transparent data-[state=active]:border-primary"
|
||||
>
|
||||
Agentes Aduanales
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value="customs"
|
||||
class="data-[state=active]:border-primary border-b-2 border-transparent rounded-none"
|
||||
class="rounded-none border-b-2 border-transparent data-[state=active]:border-primary"
|
||||
>
|
||||
Secciones Aduanales
|
||||
</Tabs.Trigger>
|
||||
@@ -214,13 +213,11 @@
|
||||
|
||||
<Tabs.Content
|
||||
value="brokers"
|
||||
class="flex-1 flex gap-4 overflow-hidden mt-0 data-[state=inactive]:hidden"
|
||||
class="mt-0 flex flex-1 gap-4 overflow-hidden data-[state=inactive]:hidden"
|
||||
>
|
||||
<!-- Left Panel: Table -->
|
||||
<div class="flex-1 flex flex-col gap-4 overflow-hidden">
|
||||
<!-- Filters -->
|
||||
<div class="border rounded-lg bg-card">
|
||||
<div class="p-4 space-y-4">
|
||||
<div class="flex flex-1 flex-col gap-4 overflow-hidden">
|
||||
<div class="rounded-lg border bg-card">
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold">Filtros</h2>
|
||||
<span class="text-xs text-muted-foreground">Busque por nombre o patente</span>
|
||||
@@ -244,23 +241,20 @@
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<!-- Placeholder for layout balance -->
|
||||
</div>
|
||||
<div class="flex items-end"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="flex-1 flex flex-col border rounded-lg overflow-hidden">
|
||||
<div class="flex items-center justify-between p-3 border-b bg-muted/30">
|
||||
<div class="flex flex-1 flex-col overflow-hidden rounded-lg border">
|
||||
<div class="flex items-center justify-between border-b bg-muted/30 p-3">
|
||||
<h2 class="text-sm font-semibold">Listado</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{filteredItems.length} registros
|
||||
</span>
|
||||
<Button variant="outline" size="sm" onclick={loadItems}>
|
||||
<RefreshCw class="h-4 w-4 mr-2" />
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
@@ -275,9 +269,8 @@
|
||||
idField="broker_key"
|
||||
/>
|
||||
</div>
|
||||
<!-- Simple Pagination Controls -->
|
||||
{#if totalItems > pageSize}
|
||||
<div class="p-2 border-t flex justify-end gap-2">
|
||||
<div class="flex justify-end gap-2 border-t p-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -286,7 +279,7 @@
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<span class="flex items-center text-xs text-muted-foreground px-2">
|
||||
<span class="flex items-center px-2 text-xs text-muted-foreground">
|
||||
Página {currentPage} de {Math.ceil(totalItems / pageSize)}
|
||||
</span>
|
||||
<Button
|
||||
@@ -302,33 +295,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Details -->
|
||||
<div
|
||||
class="w-96 flex-none flex flex-col border rounded-xl bg-muted/30 shadow-sm overflow-hidden"
|
||||
class="flex w-96 flex-none flex-col overflow-hidden rounded-xl border bg-muted/30 shadow-sm"
|
||||
>
|
||||
<div class="p-4 border-b bg-card">
|
||||
<p class="text-[10px] uppercase tracking-widest opacity-80 text-muted-foreground">
|
||||
<div class="border-b bg-card p-4">
|
||||
<p class="text-[10px] tracking-widest text-muted-foreground uppercase opacity-80">
|
||||
Detalles del Agente
|
||||
</p>
|
||||
<h2
|
||||
class="text-xl font-black font-mono tracking-tighter truncate"
|
||||
class="truncate font-mono text-xl font-black tracking-tighter"
|
||||
title={selectedItem?.name || ''}
|
||||
>
|
||||
{selectedItem?.name || '---'}
|
||||
</h2>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="text-xs font-mono text-muted-foreground"
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<span class="font-mono text-xs text-muted-foreground"
|
||||
>Patente: {selectedItem?.broker_key || ''}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto p-5 space-y-6 bg-card">
|
||||
<div class="flex-1 space-y-6 overflow-auto bg-card p-5">
|
||||
{#if selectedItem}
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="space-y-1">
|
||||
<Label
|
||||
class="text-[10px] uppercase text-muted-foreground font-bold flex items-center gap-1"
|
||||
class="flex items-center gap-1 text-[10px] font-bold text-muted-foreground uppercase"
|
||||
>
|
||||
<FileText size={10} /> Licencia / Autorización
|
||||
</Label>
|
||||
@@ -338,21 +330,21 @@
|
||||
{#if selectedItem.tax_id}
|
||||
<div class="space-y-1">
|
||||
<Label
|
||||
class="text-[10px] uppercase text-muted-foreground font-bold flex items-center gap-1"
|
||||
class="flex items-center gap-1 text-[10px] font-bold text-muted-foreground uppercase"
|
||||
>
|
||||
<Hash size={10} /> RFC / Tax ID
|
||||
</Label>
|
||||
<p class="text-sm font-mono">{selectedItem.tax_id}</p>
|
||||
<p class="font-mono text-sm">{selectedItem.tax_id}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="pt-4 border-t space-y-3">
|
||||
<div class="space-y-3 border-t pt-4">
|
||||
<Label
|
||||
class="text-[10px] uppercase text-muted-foreground font-bold flex items-center gap-1"
|
||||
class="flex items-center gap-1 text-[10px] font-bold text-muted-foreground uppercase"
|
||||
>
|
||||
<MapPin size={10} /> Dirección
|
||||
</Label>
|
||||
<div class="text-sm space-y-1">
|
||||
<div class="space-y-1 text-sm">
|
||||
<p>{selectedItem.address || ''}</p>
|
||||
<p>
|
||||
{[selectedItem.city, selectedItem.state].filter(Boolean).join(', ')}
|
||||
@@ -363,9 +355,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t space-y-3">
|
||||
<div class="space-y-3 border-t pt-4">
|
||||
<Label
|
||||
class="text-[10px] uppercase text-muted-foreground font-bold flex items-center gap-1"
|
||||
class="flex items-center gap-1 text-[10px] font-bold text-muted-foreground uppercase"
|
||||
>
|
||||
<Phone size={10} /> Contacto
|
||||
</Label>
|
||||
@@ -391,9 +383,9 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center h-full text-center text-muted-foreground opacity-50"
|
||||
class="flex h-full flex-col items-center justify-center text-center text-muted-foreground opacity-50"
|
||||
>
|
||||
<Building2 class="h-12 w-12 mb-3" />
|
||||
<Building2 class="mb-3 h-12 w-12" />
|
||||
<p class="text-sm">Selecciona un agente</p>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -401,9 +393,8 @@
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="customs" class="flex-1 overflow-auto mt-0 data-[state=inactive]:hidden">
|
||||
<!-- Reusing DataTable from Customs Sections -->
|
||||
<Card.Root class="h-full flex flex-col border-none shadow-none">
|
||||
<Tabs.Content value="customs" class="mt-0 flex-1 overflow-auto data-[state=inactive]:hidden">
|
||||
<Card.Root class="flex h-full flex-col border-none shadow-none">
|
||||
<Card.Content class="flex-1 p-0">
|
||||
<SectionsDataTable
|
||||
data={sections}
|
||||
@@ -418,13 +409,13 @@
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
<div
|
||||
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] ml-[calc(var(--sidebar-width))]"
|
||||
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
|
||||
>
|
||||
<div class="px-4 py-4 max-w-[1400px] mx-auto">
|
||||
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
||||
<div class="flex justify-end gap-2">
|
||||
{#if activeTab === 'brokers'}
|
||||
<Button size="sm" href="/dashboard/customs_brokers/edit">
|
||||
<Plus class="h-4 w-4 mr-1" />
|
||||
<Button size="sm" href="/dashboard/customs_brokers/edit/new">
|
||||
<Plus class="mr-1 h-4 w-4" />
|
||||
Nuevo
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onclick={handleEdit} disabled={!selectedItem}>
|
||||
@@ -441,7 +432,7 @@
|
||||
</Button>
|
||||
{:else}
|
||||
<Button size="sm" onclick={() => toast.info('Pendiente')}>
|
||||
<Plus class="h-4 w-4 mr-1" />
|
||||
<Plus class="mr-1 h-4 w-4" />
|
||||
Nueva Sección
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
|
||||
// Importar componentes de la librería
|
||||
import DataTable from '$lib/components/dashboard/transportation/trailers/data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/transportation/trailers/create-edit-dialog.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/transportation/trailers/columns';
|
||||
|
||||
import { trailersApi, type Trailer } from '$lib/api/dashboard/a76/trailers';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// --- ESTADO ---
|
||||
let data = $state<Trailer[]>([]);
|
||||
let totalItems = $state(0);
|
||||
let pageCount = $state(0);
|
||||
let loading = $state(false);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
let currentPage = $derived(Number($page.url.searchParams.get('page')) || 1);
|
||||
let pageSize = 10;
|
||||
|
||||
// Filtros
|
||||
let searchNumber = $state($page.url.searchParams.get('trailer_number') || '');
|
||||
let searchPlate = $state($page.url.searchParams.get('plate_number') || '');
|
||||
let searchTimeout: NodeJS.Timeout;
|
||||
|
||||
// --- LOGICA ---
|
||||
async function loadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
try {
|
||||
const response = await trailersApi.list(companyStore.activeCompany.id, {
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
trailer_number: searchNumber,
|
||||
plate_number: searchPlate
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
data = response.data.items;
|
||||
totalItems = response.data.total;
|
||||
pageCount = Math.ceil(response.data.total / response.data.page_size);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading trailers:', error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', '1');
|
||||
|
||||
if (searchNumber) url.searchParams.set('trailer_number', searchNumber);
|
||||
else url.searchParams.delete('trailer_number');
|
||||
|
||||
if (searchPlate) url.searchParams.set('plate_number', searchPlate);
|
||||
else url.searchParams.delete('plate_number');
|
||||
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Recargar datos cuando cambia el contexto de compañía o la página/filtros
|
||||
$effect(() => {
|
||||
const _ = { p: $page.url.href, c: companyStore.activeCompany?.id };
|
||||
loadData();
|
||||
});
|
||||
|
||||
const columns = createColumns(loadData);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col space-y-6 p-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Trailers</h2>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de trailers de la compañía</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Trailer
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<Input
|
||||
placeholder="Buscar por número..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchNumber}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Buscar por placas..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchPlate}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if loading && data.length === 0}
|
||||
<div class="flex h-64 items-center justify-center text-muted-foreground">
|
||||
Cargando trailers...
|
||||
</div>
|
||||
{:else}
|
||||
<DataTable {data} {columns} {pageCount} {totalItems} />
|
||||
{/if}
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={loadData} />
|
||||
</div>
|
||||
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
|
||||
// Importar componentes de la librería
|
||||
import DataTable from '$lib/components/dashboard/transportation/transporters/data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/transportation/transporters/transporter-columns';
|
||||
|
||||
import { transportersApi, type Transporter } from '$lib/api/dashboard/a76/transporters';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// --- ESTADO ---
|
||||
let data = $state<Transporter[]>([]);
|
||||
let totalItems = $state(0);
|
||||
let pageCount = $state(0);
|
||||
let loading = $state(false);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
let currentPage = $derived(Number($page.url.searchParams.get('page')) || 1);
|
||||
let pageSize = 10;
|
||||
|
||||
// Filtros
|
||||
let searchKey = $state($page.url.searchParams.get('transporter_key') || '');
|
||||
let searchName = $state($page.url.searchParams.get('name') || '');
|
||||
let searchTimeout: NodeJS.Timeout;
|
||||
|
||||
// --- LOGICA ---
|
||||
async function loadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
try {
|
||||
const response = await transportersApi.list(companyStore.activeCompany.id, {
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
transporter_key: searchKey,
|
||||
name: searchName
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
data = response.data.items;
|
||||
totalItems = response.data.total;
|
||||
pageCount = Math.ceil(response.data.total / response.data.page_size);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading transporters:', error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', '1');
|
||||
|
||||
if (searchKey) url.searchParams.set('transporter_key', searchKey);
|
||||
else url.searchParams.delete('transporter_key');
|
||||
|
||||
if (searchName) url.searchParams.set('name', searchName);
|
||||
else url.searchParams.delete('name');
|
||||
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Recargar datos cuando cambia el contexto de compañía o la página/filtros
|
||||
$effect(() => {
|
||||
const _ = { p: $page.url.href, c: companyStore.activeCompany?.id };
|
||||
loadData();
|
||||
});
|
||||
|
||||
const columns = createColumns(loadData);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col space-y-6 p-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Transportistas</h2>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de líneas transportistas</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Transportista
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<Input
|
||||
placeholder="Buscar por clave..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchKey}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Buscar por nombre..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchName}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if loading && data.length === 0}
|
||||
<div class="flex h-64 items-center justify-center text-muted-foreground">
|
||||
Cargando transportistas...
|
||||
</div>
|
||||
{:else}
|
||||
<DataTable {data} {columns} {pageCount} {totalItems} />
|
||||
{/if}
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={loadData} />
|
||||
</div>
|
||||
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
|
||||
// Importar componentes de la librería
|
||||
import DataTable from '$lib/components/dashboard/transportation/vehicles/data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/transportation/vehicles/create-edit-dialog.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/transportation/vehicles/columns';
|
||||
|
||||
import { vehiclesApi, type Vehicle } from '$lib/api/dashboard/a76/vehicles';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// --- ESTADO ---
|
||||
let data = $state<Vehicle[]>([]);
|
||||
let totalItems = $state(0);
|
||||
let pageCount = $state(0);
|
||||
let loading = $state(false);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
let currentPage = $derived(Number($page.url.searchParams.get('page')) || 1);
|
||||
let pageSize = 10;
|
||||
|
||||
// Filtros
|
||||
let searchKey = $state($page.url.searchParams.get('vehicle_key') || '');
|
||||
let searchPlate = $state($page.url.searchParams.get('plate_number') || '');
|
||||
let searchTimeout: NodeJS.Timeout;
|
||||
|
||||
// --- LOGICA ---
|
||||
async function loadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
try {
|
||||
const response = await vehiclesApi.list(companyStore.activeCompany.id, {
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
vehicle_key: searchKey,
|
||||
plate_number: searchPlate
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
data = response.data.items;
|
||||
totalItems = response.data.total;
|
||||
pageCount = Math.ceil(response.data.total / response.data.page_size);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading vehicles:', error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', '1');
|
||||
|
||||
if (searchKey) url.searchParams.set('vehicle_key', searchKey);
|
||||
else url.searchParams.delete('vehicle_key');
|
||||
|
||||
if (searchPlate) url.searchParams.set('plate_number', searchPlate);
|
||||
else url.searchParams.delete('plate_number');
|
||||
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Recargar datos cuando cambia el contexto de compañía o la página/filtros
|
||||
$effect(() => {
|
||||
const _ = { p: $page.url.href, c: companyStore.activeCompany?.id };
|
||||
loadData();
|
||||
});
|
||||
|
||||
const columns = createColumns(loadData);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col space-y-6 p-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Vehículos (Transporte)</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Gestión del catálogo de camiones y vehículos de transporte
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Vehículo
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<Input
|
||||
placeholder="Buscar por clave..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchKey}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Buscar por placas..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchPlate}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if loading && data.length === 0}
|
||||
<div class="flex h-64 items-center justify-center text-muted-foreground">
|
||||
Cargando vehículos...
|
||||
</div>
|
||||
{:else}
|
||||
<DataTable {data} {columns} {pageCount} {totalItems} />
|
||||
{/if}
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={loadData} />
|
||||
</div>
|
||||
Reference in New Issue
Block a user