Merge origin/development into fix/bug-pedimento

This commit is contained in:
2026-02-26 12:27:02 -06:00
41 changed files with 3281 additions and 298 deletions

View File

@@ -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)}
/>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>