Merge branch 'development' of https://git.aduanasoft.com/ADUANASOFT/anexo76 into development

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
This commit is contained in:
2026-02-11 17:38:10 -06:00
20 changed files with 1424 additions and 456 deletions

View File

@@ -2,101 +2,362 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { getDodas, deleteDoda, type Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
import * as Card from '$lib/components/ui/card';
import { toast } from 'svelte-sonner';
import {
Plus,
RefreshCw,
Trash2,
Pencil,
Search,
RotateCcw,
FileText,
LayoutGrid,
Printer
} from 'lucide-svelte';
import * as Select from '$lib/components/ui/select';
import { companyStore } from '$lib/stores/company.svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/doda/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/doda/columns';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/doda/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { invalidateAll } from '$app/navigation';
import { Label } from '$lib/components/ui/label';
import { Separator } from '$lib/components/ui/separator';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaDoda } from '$lib/config/shortcuts/dashboard/general_catalogs/doda/list';
import type { Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchIntegration = $state($page.url.searchParams.get('integration_number') || '');
// Filtros centralizados
let filters = $state({
integration_number: $page.url.searchParams.get('integration_number') || '',
patent: $page.url.searchParams.get('patent') || '',
status: $page.url.searchParams.get('status') || '',
operation_type: $page.url.searchParams.get('operation_type') || ''
});
let timeout: ReturnType<typeof setTimeout>;
// State for infinite scroll
let allItems = $state<Doda[]>(data.dodas?.items || []);
let currentPage = $state(data.dodas?.page || 1);
let pageSize = $state(50);
let totalItems = $state(data.dodas?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Selection
let selectedId = $state<number | null>(null);
const selectedDoda = $derived(selectedId ? allItems.find((i) => i.id === selectedId) : null);
// Funciones de acción
function handleCreateClick() {
goto('/dashboard/general_catalogs/doda/edit');
}
// Sincronizar con datos del servidor al cargar (primera carga)
$effect(() => {
if (data.dodas) {
allItems = data.dodas.items || [];
currentPage = data.dodas.page || 1;
totalItems = data.dodas.total || 0;
}
});
// Sincronizar filtros con la URL de forma reactiva
$effect(() => {
if (browser) {
const params = new URLSearchParams();
if (filters.integration_number) params.set('integration_number', filters.integration_number);
if (filters.patent) params.set('patent', filters.patent);
if (filters.status) params.set('status', filters.status);
if (filters.operation_type) params.set('operation_type', filters.operation_type);
const queryString = params.toString();
const newUrl = queryString ? `?${queryString}` : window.location.pathname;
if (window.location.search !== (queryString ? `?${queryString}` : '')) {
window.history.replaceState({}, '', newUrl);
}
}
});
// Disparar recarga cuando cambian los filtros (con debounce)
$effect(() => {
// Observamos todos los campos de filtros
const _ = { ...filters };
clearTimeout(timeout);
timeout = setTimeout(() => {
reloadData();
}, 400);
});
// Atajos
useShortcuts(
'Lista DODA',
obtenerAtajosListaDoda({
manejarNuevo: handleCreateClick,
manejarActualizar: handleSuccess
manejarActualizar: reloadData
})
);
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchIntegration) url.searchParams.set('integration_number', searchIntegration);
else url.searchParams.delete('integration_number');
async function loadMore() {
if (loading || !hasMore) return;
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
loading = true;
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
// Limpiar filtros vacíos
const activeFilters = Object.fromEntries(
Object.entries(filters).filter(([_, v]) => v !== '')
);
const res = await getDodas(currentPage + 1, pageSize, activeFilters, Number(companyId));
if (res.data) {
allItems = [...allItems, ...res.data.items];
currentPage++;
totalItems = res.data.total;
}
} catch (e) {
console.error('Error loading more DODAs:', e);
} finally {
loading = false;
}
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
selectedId = null;
async function reloadData() {
if (!browser) return;
loading = true;
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
const activeFilters = Object.fromEntries(
Object.entries(filters).filter(([_, v]) => v !== '')
);
const res = await getDodas(1, pageSize, activeFilters, Number(companyId));
if (res.data) {
allItems = res.data.items;
currentPage = 1;
totalItems = res.data.total;
selectedId = null;
}
} catch (e) {
console.error('Error reloading DODAs:', e);
// Evitar mostrar error si es solo carga inicial y falla por falta de login etc
if (allItems.length > 0) toast.error('Error al recargar datos');
} finally {
loading = false;
}
}
function clearFilters() {
filters = {
integration_number: '',
patent: '',
status: '',
operation_type: ''
};
}
function handleCreateClick() {
goto('/dashboard/general_catalogs/doda/edit');
}
function handleEdit() {
if (selectedId) {
goto(`/dashboard/general_catalogs/doda/edit/${selectedId}`);
}
}
async function handleDelete() {
if (!selectedId || !companyStore.activeCompany) return;
if (confirm('¿Estás seguro de eliminar este DODA?')) {
try {
await deleteDoda(selectedId, companyStore.activeCompany.id);
toast.success('DODA eliminado correctamente');
reloadData();
} catch (e) {
toast.error('Error al eliminar DODA');
}
}
}
function handleRowClick(doda: Doda) {
selectedId = doda.id;
selectedId = selectedId === doda.id ? null : doda.id;
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">DODA</h1>
<p class="text-muted-foreground">Gestión de Documentos de Operación de Aduana</p>
<h1 class="text-3xl font-bold tracking-tight">DODA</h1>
<p class="text-muted-foreground">Gestiona tus Documentos de Operación Aduanera (DODA)</p>
</div>
<!-- <Button onclick={() => dialogOpen = true}> -->
<Button href="/dashboard/general_catalogs/doda/edit">
<Button onclick={handleCreateClick} class="shadow-sm transition-all hover:translate-y-[-1px]">
<Plus class="mr-2 h-4 w-4" />
Nuevo DODA
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por No. Integración..."
bind:value={searchIntegration}
oninput={handleSearch}
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Filtros Avanzados</Card.Title>
<Card.Description>Refina tu búsqueda mediante múltiples criterios</Card.Description>
</div>
<Button variant="ghost" size="sm" onclick={clearFilters} class="text-muted-foreground">
<RotateCcw class="mr-2 h-4 w-4" />
Limpiar Filtros
</Button>
</div>
</Card.Header>
<Card.Content>
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<div class="space-y-2">
<Label for="search-integration">No. Integración</Label>
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-integration"
placeholder="Buscar integración..."
bind:value={filters.integration_number}
class="pl-9"
/>
</div>
</div>
<div class="space-y-2">
<Label for="search-patent">Patente</Label>
<Input id="search-patent" placeholder="Buscar patente..." bind:value={filters.patent} />
</div>
<div class="space-y-2">
<Label>Estatus</Label>
<Select.Root
type="single"
value={filters.status}
onValueChange={(v) => (filters.status = v)}
>
<Select.Trigger class="w-full">
{filters.status || 'Todos los estatus'}
</Select.Trigger>
<Select.Content>
<Select.Item value="">Todos</Select.Item>
<Select.Item value="PENDIENTE">PENDIENTE</Select.Item>
<Select.Item value="GENERADO">GENERADO</Select.Item>
<Select.Item value="VALIDADO">VALIDADO</Select.Item>
<Select.Item value="ELIMINADO">ELIMINADO</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="space-y-2">
<Label>Operación</Label>
<Select.Root
type="single"
value={filters.operation_type}
onValueChange={(v) => (filters.operation_type = v)}
>
<Select.Trigger class="w-full">
{filters.operation_type === 'I'
? 'Importación'
: filters.operation_type === 'E'
? 'Exportación'
: 'Todas'}
</Select.Trigger>
<Select.Content>
<Select.Item value="">Todas</Select.Item>
<Select.Item value="I">I - Importación</Select.Item>
<Select.Item value="E">E - Exportación</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de DODAs</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" size="sm" onclick={reloadData} disabled={loading}>
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content class="p-0">
<DataTable
data={allItems}
columns={createColumns(reloadData)}
{loading}
{hasMore}
{loadMore}
{selectedId}
onRowClick={handleRowClick}
/>
</Card.Content>
</Card.Root>
<!-- Footer fijo de acciones (estilo Facturas) -->
<div
class="fixed right-0 bottom-0 left-0 z-50 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="mx-auto max-w-[1400px] px-4 py-4">
<div class="flex items-center justify-between">
<div class="text-sm text-muted-foreground">
{#if selectedDoda}
Seleccionado: <span class="font-medium text-foreground"
>{selectedDoda.integration_number || 'S/N'}</span
>
{:else}
Selecciona un registro para ver acciones
{/if}
</div>
<div class="flex gap-2">
<Button variant="outline" size="sm" onclick={reloadData} disabled={loading}>
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
Actualizar
</Button>
<Button variant="outline" size="sm" onclick={handleEdit} disabled={!selectedId}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</Button>
<Button variant="destructive" size="sm" onclick={handleDelete} disabled={!selectedId}>
<Trash2 class="mr-2 h-4 w-4" />
Eliminar
</Button>
<Separator orientation="vertical" class="mx-1 h-8" />
<Button variant="secondary" size="sm" disabled={!selectedId}>
<Printer class="mr-2 h-4 w-4" />
Imprimir
</Button>
</div>
</div>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.dodas?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.dodas?.pages || 0}
totalItems={data.dodas?.total || 0}
{selectedId}
onRowClick={handleRowClick}
/>
</div>
{#if dialogOpen}
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
{/if}
</div>
<div class="h-20"></div>
<!-- Espacio buffer para el footer -->
{#if dialogOpen}
<CreateEditDialog bind:open={dialogOpen} onSuccess={reloadData} />
{/if}

View File

@@ -8,6 +8,9 @@
import { Switch } from '$lib/components/ui/switch';
import * as Tabs from '$lib/components/ui/tabs';
import * as Card from '$lib/components/ui/card';
import * as Select from '$lib/components/ui/select';
import * as RadioGroup from '$lib/components/ui/radio-group';
import { Separator } from '$lib/components/ui/separator';
import { companyStore } from '$lib/stores/company.svelte';
import {
createDoda,
@@ -15,19 +18,42 @@
getDoda,
type DodaCreate
} from '$lib/api/dashboard/a76/general_catalogs/doda';
import { ArrowLeft, LoaderCircle, Save } from 'lucide-svelte';
import {
ArrowLeft,
Save,
RefreshCw,
FileText,
LayoutGrid,
Printer,
Trash2,
FolderSearch,
ShieldCheck,
LoaderCircle
} from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosFormularioDoda } from '$lib/config/shortcuts/dashboard/general_catalogs/doda/edit';
import ChildDetailTable from '$lib/components/dashboard/general_catalogs/doda/child-detail-table.svelte';
// Modales de Selección
import BrokerSelectorDialog from '$lib/components/dashboard/export/manifest/modals/broker-selector-dialog.svelte';
import CustomsSectionSelectorDialog from '$lib/components/dashboard/shared/modals/customs-section-selector-dialog.svelte';
import TransporterSelectorDialog from '$lib/components/dashboard/export/manifest/modals/transporter-selector-dialog.svelte';
// 1. Identificación reactiva
let id = $derived($page.params.id);
let isEdit = $derived(!!id);
let isEdit = $derived(!!$page.params.id);
let title = $derived(isEdit ? 'Editar DODA' : 'Nuevo DODA');
let loading = $state(false);
let error = $state<string | null>(null);
let activeTab = $state('general');
// Estados de Modales
let showBrokerSelector = $state(false);
let showAduanaSelector = $state(false);
let showSectionSelector = $state(false);
let showTransporterSelector = $state(false);
// Atajos
useShortcuts(
'Formulario DODA',
@@ -38,7 +64,12 @@
})
);
function getEmptyForm(): DodaCreate {
function getEmptyForm(): DodaCreate & {
pedimentos_detail?: any[];
containers?: any[];
american_pedimentos?: any[];
uuid_carta_porte?: string;
} {
return {
integration_number: '',
doda_date: undefined,
@@ -50,7 +81,7 @@
caat: '',
transport_identification: '',
fast_id: '',
operation_type: '',
operation_type: 'I',
selected: false,
user_selected: '',
last_user: '',
@@ -62,24 +93,35 @@
serial_number: '',
electronic_signature: '',
transaction_number: '',
status: '',
status: 'PENDIENTE',
linq_sat_qr: '',
sat_certificate: '',
sat_digital_seal: '',
xml_doda_sent_path: '',
xml_doda_response_path: '',
sat_original_chain: '',
customs_clearance: undefined,
unique_badge_number: ''
customs_clearance: 2, // 2 = DODA, 1 = PITA
unique_badge_number: '',
pedimentos_detail: [],
containers: [],
american_pedimentos: [],
uuid_carta_porte: ''
};
}
let formData = $state<DodaCreate>(getEmptyForm());
let formData = $state(getEmptyForm());
$effect(() => {
if (id) {
loadDoda(Number(id));
} else {
const currentId = $page.params.id;
const companyId = companyStore.activeCompany?.id;
console.log('DEBUG: Effect triggered', { currentId, companyId });
if (currentId && companyId) {
console.log('DEBUG: Calling loadDoda with', currentId);
loadDoda(Number(currentId));
} else if (!currentId) {
console.log('DEBUG: No ID, resetting form');
formData = getEmptyForm();
error = null;
}
@@ -89,7 +131,15 @@
loading = true;
try {
const companyId = companyStore.activeCompany?.id;
console.log('DEBUG: loadDoda executing', { dodaId, companyId });
if (!companyId) {
console.error('DEBUG: No company ID available in loadDoda');
return;
}
const data = await getDoda(dodaId, companyId);
console.log('DEBUG: getDoda response', data);
if (data) {
formData = {
@@ -103,7 +153,7 @@
caat: data.caat || '',
transport_identification: data.transport_identification || '',
fast_id: data.fast_id || '',
operation_type: data.operation_type || '',
operation_type: data.operation_type || 'I',
selected: data.selected || false,
user_selected: data.user_selected || '',
last_user: data.last_user || '',
@@ -115,15 +165,19 @@
serial_number: data.serial_number || '',
electronic_signature: data.electronic_signature || '',
transaction_number: data.transaction_number || '',
status: data.status || '',
status: data.status || 'PENDIENTE',
linq_sat_qr: data.linq_sat_qr || '',
sat_certificate: data.sat_certificate || '',
sat_digital_seal: data.sat_digital_seal || '',
xml_doda_sent_path: data.xml_doda_sent_path || '',
xml_doda_response_path: data.xml_doda_response_path || '',
sat_original_chain: data.sat_original_chain || '',
customs_clearance: data.customs_clearance,
unique_badge_number: data.unique_badge_number || ''
customs_clearance: data.customs_clearance || 2,
unique_badge_number: data.unique_badge_number || '',
pedimentos_detail: data.pedimentos_detail || [],
containers: data.containers || [],
american_pedimentos: data.american_pedimentos || [],
uuid_carta_porte: '' // Simulated field
};
}
} catch (e) {
@@ -139,13 +193,11 @@
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) throw new Error('Selecciona una compañía');
if (!formData.integration_number?.trim())
throw new Error('El número de integración es requerido');
if (!formData.patent?.trim()) throw new Error('El Agente Aduanal (Patente) es requerido');
const payload: DodaCreate = {
...formData,
integration_number: formData.integration_number.trim(),
integration_number: formData.integration_number?.trim() || '',
doda_date: formData.doda_date || undefined,
doda_time: formData.doda_time || undefined,
customs_clearance: formData.customs_clearance || undefined
@@ -164,260 +216,524 @@
loading = false;
}
}
const pedimentosColumns = [
{ header: 'Doda Sysid', key: 'id' },
{ header: 'Línea Pedimento', key: 'pedimento_line' },
{ header: 'Patente Autorización', key: 'authorization_patent' },
{ header: 'Documento', key: 'document' },
{ header: 'Remesa', key: 'shipment' },
{ header: 'Cove', key: 'cove' },
{ header: 'UMC', key: 'umc' },
{ header: 'Importe Efectivo USD', key: 'effective_amount_usd' },
{ header: 'Importe Diferencia USD', key: 'difference_amount_usd' },
{ header: 'DTA NIU', key: 'dta_niu' },
{ header: 'Articulo 7', key: 'article_7', render: (v: any) => (v ? 'Sí' : 'No') }
];
const containersColumns = [
{ header: 'Contenedor', key: 'container_value' },
{ header: 'Percinto', key: 'seals' }
];
const americanPedimentosColumns = [
{ header: 'Tipo', key: 'american_pedimento_type' },
{ header: 'Pedido Americano', key: 'american_pedimento_value' }
];
// Handlers de Selección
function handleBrokerSelect(broker: any) {
formData.responsible = broker.broker_key || '';
formData.patent = broker.license || '';
}
function handleAduanaSelect(section: any) {
formData.dispatch_customs = section.customs_code || '';
}
function handleSectionSelect(section: any) {
formData.customs_sections = section.customs_code || '';
}
function handleTransporterSelect(transporter: any) {
formData.carrier = transporter.name || '';
}
</script>
<div class="w-full mx-auto max-w-6xl py-6 px-4 space-y-6 pb-48">
<div class="flex items-center gap-4">
<Button variant="outline" size="icon" href="/dashboard/general_catalogs/doda">
<ArrowLeft class="h-4 w-4" />
</Button>
<div>
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
<div class="space-y-3 p-6 pb-48">
<!-- Header Estilo Facturas -->
<div class="flex items-center justify-between">
<div class="space-y-1">
<div class="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onclick={() => goto('/dashboard/general_catalogs/doda')}
>
<ArrowLeft size={20} />
</Button>
<h1 class="text-3xl font-bold tracking-tight">{title}</h1>
</div>
<p class="text-muted-foreground">Catálogos Generales / Doda</p>
</div>
</div>
{#if error}
<div
class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium"
>
⚠️ {error}
</div>
{/if}
<Separator />
{#key id}
<form
onsubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
class="space-y-6"
>
<Card.Root>
<Card.Content class="p-6">
<Tabs.Root bind:value={activeTab} class="w-full">
<div class="min-h-[500px]">
<Tabs.Content value="general" class="space-y-4 pt-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="integration_number"
>No. Integración <span class="text-destructive">*</span></Label
>
<Input
id="integration_number"
bind:value={formData.integration_number}
maxlength={30}
/>
</div>
<div class="grid gap-2">
<Label for="status">Estatus</Label>
<Input id="status" bind:value={formData.status} />
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="grid gap-2">
<Label for="doda_date">Fecha (YYYYMMDD)</Label>
<Input
type="number"
id="doda_date"
bind:value={formData.doda_date}
placeholder="Ej: 20240101"
/>
</div>
<div class="grid gap-2">
<Label for="doda_time">Hora (HHMMSS)</Label>
<Input
type="number"
id="doda_time"
bind:value={formData.doda_time}
placeholder="Ej: 143000"
/>
</div>
<div class="grid gap-2">
<Label for="operation_type">Tipo Operación</Label>
<Input id="operation_type" bind:value={formData.operation_type} maxlength={1} />
</div>
</div>
<div class="grid gap-2">
<Label for="pedimentos">Pedimentos</Label>
<Input id="pedimentos" bind:value={formData.pedimentos} />
</div>
<div class="grid gap-2">
<Label for="pedimento_type">Tipo Pedimento</Label>
<Input id="pedimento_type" bind:value={formData.pedimento_type} />
</div>
</Tabs.Content>
<Tabs.Root bind:value={activeTab} class="w-full">
<!-- Contenido Principal con Campos Superiores -->
<div class="space-y-6">
<!-- Fila Compacta de Datos Principales (Estilo InvoiceTopFields) con líneas blancas entre campos -->
<div class="grid grid-cols-12 items-end gap-0 pb-3">
<div class="col-span-3 space-y-1 border-r border-white/20 pr-3">
<Label
class="cursor-pointer text-xs leading-none text-muted-foreground transition-colors hover:text-primary"
onclick={() => (showBrokerSelector = true)}
>
Responsable Agentes
</Label>
<div class="flex gap-2">
<Input
bind:value={formData.responsible}
class="h-8 flex-1 cursor-pointer bg-background/5 text-sm font-medium transition-colors hover:bg-background/10"
maxlength={14}
placeholder="Clave"
onclick={() => (showBrokerSelector = true)}
readonly
/>
<Button
variant="secondary"
size="icon"
type="button"
onclick={() => (showBrokerSelector = true)}
class="h-8 w-8 shrink-0 transition-colors hover:bg-background/20"
>
<FolderSearch class="h-4 w-4" />
</Button>
</div>
</div>
<div class="col-span-2 space-y-1 border-r border-white/20 px-3">
<Label
class="cursor-pointer text-xs leading-none text-muted-foreground transition-colors hover:text-primary"
onclick={() => (showAduanaSelector = true)}
>
Aduana
</Label>
<div class="flex gap-2">
<Input
bind:value={formData.dispatch_customs}
class="h-8 flex-1 cursor-pointer bg-background/5 text-sm font-medium transition-colors hover:bg-background/10"
maxlength={3}
placeholder="000"
onclick={() => (showAduanaSelector = true)}
readonly
/>
<Button
variant="outline"
size="icon"
type="button"
onclick={() => (showAduanaSelector = true)}
class="h-8 w-8 shrink-0 border-white/10 transition-colors hover:bg-background/20"
>
<FolderSearch class="h-4 w-4" />
</Button>
</div>
</div>
<div class="col-span-2 space-y-1 border-r border-white/20 px-3">
<Label
class="cursor-pointer text-xs leading-none text-muted-foreground transition-colors hover:text-primary"
onclick={() => (showSectionSelector = true)}
>
Sección
</Label>
<div class="flex gap-2">
<Input
bind:value={formData.customs_sections}
class="h-8 flex-1 cursor-pointer bg-background/5 text-sm font-medium transition-colors hover:bg-background/10"
maxlength={3}
placeholder="000"
onclick={() => (showSectionSelector = true)}
readonly
/>
<Button
variant="outline"
size="icon"
type="button"
onclick={() => (showSectionSelector = true)}
class="h-8 w-8 shrink-0 border-white/10 transition-colors hover:bg-background/20"
>
<FolderSearch class="h-4 w-4" />
</Button>
</div>
</div>
<div class="col-span-2 space-y-1 pl-3">
<Label class="text-xs leading-none text-muted-foreground">Operación</Label>
<Select.Root
type="single"
value={formData.operation_type}
onValueChange={(v) => (formData.operation_type = v)}
>
<Select.Trigger class="h-8 border-none bg-background/5 text-sm font-medium shadow-none">
<span class="truncate"
>{formData.operation_type === 'E'
? 'E'
: formData.operation_type === 'I'
? 'I'
: '...'}</span
>
</Select.Trigger>
<Select.Content>
<Select.Item value="I">I - Importación</Select.Item>
<Select.Item value="E">E - Exportación</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
<Tabs.Content value="transport" class="space-y-4 pt-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="grid gap-2">
<Label for="patent">Patente</Label>
<Input id="patent" bind:value={formData.patent} maxlength={4} />
</div>
<div class="grid gap-2">
<Label for="dispatch_customs">Aduana Despacho</Label>
<Input
id="dispatch_customs"
bind:value={formData.dispatch_customs}
maxlength={3}
/>
</div>
<div class="grid gap-2">
<Label for="customs_sections">Sección Aduanera</Label>
<Input
id="customs_sections"
bind:value={formData.customs_sections}
maxlength={3}
/>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="caat">CAAT</Label>
<Input id="caat" bind:value={formData.caat} />
</div>
<div class="grid gap-2">
<Label for="carrier">Carrier</Label>
<Input id="carrier" bind:value={formData.carrier} />
</div>
<div class="grid gap-2 md:col-span-2">
<Label for="transport_id">Ident. Transporte</Label>
<Input id="transport_id" bind:value={formData.transport_identification} />
</div>
<div class="grid gap-2">
<Label for="fast_id">FAST ID</Label>
<Input id="fast_id" bind:value={formData.fast_id} />
</div>
</div>
<div class="grid gap-2">
<Label for="shipments">Embarques (Shipments)</Label>
<Input id="shipments" bind:value={formData.shipments} />
</div>
<div class="grid gap-2">
<Label for="customs_clearance">Despacho Aduanero (ID)</Label>
<Input
type="number"
id="customs_clearance"
bind:value={formData.customs_clearance}
/>
</div>
</Tabs.Content>
{#if error}
<div
class="animate-in fade-in slide-in-from-top-2 mb-6 flex items-center gap-2 rounded-lg border border-destructive/20 bg-destructive/5 p-4 text-sm font-semibold text-destructive"
>
<span class="text-lg">⚠️</span>
{error}
</div>
{/if}
<Tabs.Content value="sat" class="space-y-4 pt-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="serial">Número de Serie</Label>
<Input id="serial" bind:value={formData.serial_number} />
</div>
<div class="grid gap-2">
<Label for="transaction">No. Transacción</Label>
<Input id="transaction" bind:value={formData.transaction_number} />
</div>
</div>
<div class="grid gap-2">
<Label for="chain">Cadena Original</Label>
<Textarea id="chain" bind:value={formData.original_chain} class="min-h-[80px]" />
</div>
<div class="grid gap-2">
<Label for="signature">Firma Electrónica</Label>
<Textarea
id="signature"
bind:value={formData.electronic_signature}
class="min-h-[80px]"
/>
</div>
<div class="grid gap-2">
<Label for="seal">Sello Digital SAT</Label>
<Textarea id="seal" bind:value={formData.sat_digital_seal} class="min-h-[80px]" />
</div>
<div class="grid gap-2">
<Label for="sat_original_chain">Cadena Original SAT</Label>
<Textarea
id="sat_original_chain"
bind:value={formData.sat_original_chain}
class="min-h-[80px]"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="xml_sent">Ruta XML Enviado</Label>
<Input id="xml_sent" bind:value={formData.xml_doda_sent_path} />
</div>
<div class="grid gap-2">
<Label for="xml_res">Ruta XML Respuesta</Label>
<Input id="xml_res" bind:value={formData.xml_doda_response_path} />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="general" class="animate-in fade-in space-y-6 duration-300 outline-none">
<!-- Grid de Campos Generales Reorganizado -->
<div class="grid grid-cols-12 items-start gap-8">
<!-- Columna 1 (Izquierda): Stack Vertical Principal -->
<div class="col-span-3 space-y-4">
<div class="space-y-1.5">
<Label
class="cursor-pointer text-xs font-semibold text-muted-foreground uppercase transition-colors hover:text-primary"
onclick={() => (showTransporterSelector = true)}
>
Transportista
</Label>
<div class="flex gap-2">
<Input
bind:value={formData.carrier}
placeholder="Transportista"
class="h-9 flex-1 cursor-pointer text-sm font-medium shadow-sm transition-colors hover:bg-background/5"
onclick={() => (showTransporterSelector = true)}
readonly
/>
<Button
variant="secondary"
size="icon"
type="button"
onclick={() => (showTransporterSelector = true)}
class="h-9 w-9 shrink-0 shadow-sm transition-all hover:translate-y-[-1px]"
>
<FolderSearch class="h-4 w-4" />
</Button>
</div>
</div>
<div class="space-y-1.5">
<Label class="text-xs font-semibold text-muted-foreground uppercase"
>Identificación</Label
>
<Input
bind:value={formData.transport_identification}
placeholder="Identificación"
class="h-9 text-sm font-medium shadow-sm"
/>
</div>
<div class="space-y-1.5">
<Label class="text-xs font-semibold text-muted-foreground uppercase"
>No. de Integración</Label
>
<Input
bind:value={formData.integration_number}
placeholder="Integración"
class="h-9 bg-muted/20 text-sm font-medium shadow-sm"
/>
</div>
<div class="space-y-1.5">
<Label class="text-xs font-semibold text-muted-foreground uppercase"
>Transacción</Label
>
<Input
bind:value={formData.transaction_number}
placeholder="Transacción"
class="h-9 text-sm font-medium shadow-sm"
/>
</div>
<div class="space-y-1.5">
<Label class="text-xs font-semibold text-muted-foreground uppercase">Fast ID</Label>
<Input
bind:value={formData.fast_id}
placeholder="Fast ID"
class="h-9 text-sm font-medium shadow-sm"
/>
</div>
</div>
<Tabs.Content value="other" class="space-y-4 pt-4">
<div class="flex items-center gap-3 p-4 border rounded-lg">
<Switch id="selected" bind:checked={formData.selected} />
<Label for="selected">DODA Seleccionado para operación</Label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="user_sel">Usuario Selección</Label>
<Input id="user_sel" bind:value={formData.user_selected} />
</div>
<div class="grid gap-2">
<Label for="last_user">Último Usuario</Label>
<Input id="last_user" bind:value={formData.last_user} />
</div>
</div>
<div class="grid gap-2">
<Label for="responsible">RFC Responsable</Label>
<Input id="responsible" bind:value={formData.responsible} maxlength={14} />
</div>
<div class="grid gap-2">
<Label for="badge">Número Único de Gafete</Label>
<Input id="badge" bind:value={formData.unique_badge_number} />
</div>
<div class="grid gap-2">
<Label for="qr">LINQ SAT QR</Label>
<Input id="qr" bind:value={formData.linq_sat_qr} />
</div>
<div class="grid gap-2">
<Label for="sat_cert">Certificado SAT</Label>
<Input id="sat_cert" bind:value={formData.sat_certificate} />
</div>
</Tabs.Content>
<!-- Columna 4 (Alineada con Operación): Stack de Estatus/Patente -->
<div class="col-span-3 col-start-8 space-y-4">
<div class="space-y-1.5">
<Label class="text-xs font-semibold text-muted-foreground uppercase">CAAT</Label>
<Input
bind:value={formData.caat}
placeholder="CAAT"
class="h-9 text-sm font-medium shadow-sm"
/>
</div>
<div class="space-y-1.5">
<Label
class="cursor-pointer text-xs font-semibold text-muted-foreground uppercase transition-colors hover:text-primary"
onclick={() => (showBrokerSelector = true)}
>
Patente <span class="text-destructive">*</span>
</Label>
<div class="flex gap-2">
<Input
bind:value={formData.patent}
maxlength={4}
placeholder="Patente"
class="h-9 flex-1 cursor-pointer text-sm font-medium shadow-sm transition-colors hover:bg-background/5"
onclick={() => (showBrokerSelector = true)}
readonly
/>
<Button
variant="outline"
size="icon"
type="button"
onclick={() => (showBrokerSelector = true)}
class="h-9 w-9 shrink-0 shadow-sm transition-all hover:translate-y-[-1px]"
>
<FolderSearch class="h-4 w-4" />
</Button>
</div>
</div>
<div class="space-y-1.5">
<Label class="text-xs font-semibold text-muted-foreground uppercase">Estatus</Label>
<Select.Root
type="single"
value={formData.status}
onValueChange={(v) => (formData.status = v)}
>
<Select.Trigger class="h-9 w-full text-sm font-medium shadow-sm">
{formData.status || 'Seleccionar...'}
</Select.Trigger>
<Select.Content>
<Select.Item value="PENDIENTE">PENDIENTE</Select.Item>
<Select.Item value="GENERADO">GENERADO</Select.Item>
<Select.Item value="ELIMINADO">ELIMINADO</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
<!-- Fila Horizontal: Despacho y Gafete (A la derecha de Fast ID) -->
<div class="col-span-12 grid grid-cols-12 items-end gap-8">
<div class="col-span-3">
<!-- Espacio vacío para alinear con la primera columna si es necesario -->
</div>
<Tabs.List
class="grid grid-cols-4 fixed bottom-24 left-1/2 -translate-x-1/2 w-[95%] max-w-2xl z-40 shadow-2xl bg-background border p-1 rounded-xl"
>
<Tabs.Trigger value="general">General</Tabs.Trigger>
<Tabs.Trigger value="transport">Aduana</Tabs.Trigger>
<Tabs.Trigger value="sat">SAT</Tabs.Trigger>
<Tabs.Trigger value="other">Otros</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
</Card.Content>
</Card.Root>
<!-- Despacho Aduanero (A la derecha de Fast ID en términos lógicos) -->
<div class="col-span-4">
<div
class="flex items-center justify-between rounded-xl border bg-muted/30 p-4 shadow-inner"
>
<Label class="text-xs font-semibold tracking-widest text-muted-foreground uppercase"
>Despacho Aduanero</Label
>
<RadioGroup.Root
value={formData.customs_clearance?.toString()}
onValueChange={(v) => (formData.customs_clearance = parseInt(v))}
class="flex gap-6"
>
<div class="flex cursor-pointer items-center space-x-2">
<RadioGroup.Item value="1" id="pita" class="h-4 w-4 border-primary" />
<Label for="pita" class="cursor-pointer text-xs font-medium uppercase"
>PITA</Label
>
</div>
<div class="flex cursor-pointer items-center space-x-2">
<RadioGroup.Item value="2" id="doda" class="h-4 w-4 border-primary" />
<Label for="doda" class="cursor-pointer text-xs font-medium uppercase"
>DODA</Label
>
</div>
</RadioGroup.Root>
</div>
</div>
<div
class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-inner"
>
<div class="max-w-6xl mx-auto flex justify-end gap-4 px-4 w-full">
<!-- Gafete Único (A la derecha de Despacho) -->
<div class="col-span-4">
<div class="space-y-1.5">
<Label class="text-xs font-semibold text-muted-foreground uppercase"
>Número de gafete único</Label
>
<Input
bind:value={formData.unique_badge_number}
placeholder="Gafete único"
class="h-9 text-sm font-medium shadow-sm"
/>
</div>
</div>
</div>
</div>
<!-- Tabla Principal -->
<div class="pt-4">
<ChildDetailTable
title="Detalle de Pedimentos"
columns={pedimentosColumns}
data={formData.pedimentos_detail || []}
class="border-border bg-card shadow-sm"
/>
</div>
<!-- Tablas Inferiores -->
<div class="grid grid-cols-1 gap-6 pt-4 lg:grid-cols-2">
<ChildDetailTable
title="Contenedores"
columns={containersColumns}
data={formData.containers || []}
/>
<ChildDetailTable
title="Pedimento Americano"
columns={americanPedimentosColumns}
data={formData.american_pedimentos || []}
/>
</div>
</Tabs.Content>
<Tabs.Content value="sellos" class="animate-in fade-in duration-300 outline-none">
<div class="max-w-4xl space-y-8 py-4">
<div class="space-y-1">
<h2 class="text-xl font-bold tracking-tight">Sellos y Firmas</h2>
<p class="text-xs font-semibold text-muted-foreground uppercase">
Validación electrónica ante el SAT
</p>
</div>
<div class="grid grid-cols-1 gap-8">
<div class="space-y-2">
<Label class="text-xs font-semibold tracking-wide text-muted-foreground uppercase"
>Cadena original</Label
>
<Textarea
bind:value={formData.original_chain}
placeholder="Cadena Original..."
class="min-h-[120px] w-full border-muted/60 font-mono text-xs font-medium shadow-none"
/>
</div>
<div class="space-y-2">
<Label class="text-xs font-semibold tracking-wide text-muted-foreground uppercase"
>Número de certificado</Label
>
<Input
bind:value={formData.serial_number}
placeholder="Certificado"
class="w-full border-muted/60 text-sm font-medium shadow-none"
/>
</div>
<div class="space-y-2">
<Label class="text-xs font-semibold tracking-wide text-muted-foreground uppercase"
>Firma electrónica</Label
>
<Textarea
bind:value={formData.electronic_signature}
placeholder="Firma..."
class="min-h-[100px] w-full border-muted/60 font-mono text-xs font-medium shadow-none"
/>
</div>
<div class="space-y-2">
<Label class="text-xs font-semibold tracking-wide text-primary uppercase"
>UUID Carta Porte</Label
>
<Input
bind:value={formData.uuid_carta_porte}
placeholder="00000000-0000-0000-0000-000000000000"
class="w-full border-muted/60 font-mono text-sm font-medium shadow-none"
/>
</div>
<div class="space-y-2">
<Label class="text-xs font-semibold tracking-wide text-muted-foreground uppercase"
>Número certificado SAT</Label
>
<Input
bind:value={formData.sat_certificate}
placeholder="Certificado SAT"
class="w-full border-muted/60 text-sm font-medium shadow-none"
/>
</div>
<div class="space-y-2">
<Label class="text-xs font-semibold tracking-wide text-muted-foreground uppercase"
>Firma electrónica SAT (Cadena Original SAT)</Label
>
<Textarea
bind:value={formData.sat_original_chain}
placeholder="Firma SAT..."
class="min-h-[120px] w-full border-muted/60 font-mono text-xs font-medium shadow-none"
/>
</div>
</div>
</div>
</Tabs.Content>
</div>
<!-- Footer fijo con Tabs.List y Botones de Acción -->
<div
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="mx-auto max-w-[1400px] space-y-4 px-4 py-4">
<div class="w-full overflow-x-auto pb-2">
<Tabs.List class="inline-flex md:grid md:w-full md:grid-cols-2">
<Tabs.Trigger value="general" class="whitespace-nowrap">
<LayoutGrid size={16} class="mr-2" />
General
</Tabs.Trigger>
<Tabs.Trigger value="sellos" class="whitespace-nowrap">
<ShieldCheck size={16} class="mr-2" />
Sellos
</Tabs.Trigger>
</Tabs.List>
</div>
<div class="flex justify-end gap-3">
<Button
type="button"
variant="ghost"
href="/dashboard/general_catalogs/doda"
variant="outline"
onclick={() => goto('/dashboard/general_catalogs/doda')}
disabled={loading}
class="rounded px-8 font-medium"
>
Cancelar
</Button>
<Button type="submit" disabled={loading} class="min-w-[140px]">
<Button
onclick={handleSubmit}
disabled={loading}
class="min-w-[200px] rounded font-bold uppercase shadow-sm"
>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Guardando...
{:else}
<Save class="mr-2 h-4 w-4" />
{isEdit ? 'Actualizar' : 'Guardar'}
{isEdit ? 'Guardar Cambios' : 'Crear DODA'}
{/if}
</Button>
</div>
</div>
</form>
{/key}
</div>
</Tabs.Root>
<!-- Diálogos de Selección de Catálogos -->
<BrokerSelectorDialog bind:open={showBrokerSelector} onSelect={handleBrokerSelect} />
<CustomsSectionSelectorDialog bind:open={showAduanaSelector} onSelect={handleAduanaSelect} />
<CustomsSectionSelectorDialog bind:open={showSectionSelector} onSelect={handleSectionSelect} />
<TransporterSelectorDialog
bind:open={showTransporterSelector}
onSelect={handleTransporterSelect}
/>
</div>

View File

@@ -24,7 +24,7 @@
} from 'lucide-svelte';
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { focusStore } from '$lib/stores/focus-store';
import { focusStore, interactionMode } from '$lib/stores/focus-store';
import { obtenerAtajosPrincipalesEdicionPedimento } from '$lib/config/shortcuts/dashboard/pedimentos/edit';
// Importar los componentes de cada pestaña (ahora sin botones de guardar propios)
@@ -43,13 +43,17 @@
import ValidationTabForm from '$lib/components/dashboard/pedimentos/edit/validation-tab-form.svelte';
// Importar solo la API de pedimentos
import { pedimentosApi, type CreatePedimentoData, type UpdatePedimentoData } from '$lib/api/dashboard/a76/pedimentos';
import {
pedimentosApi,
type CreatePedimentoData,
type UpdatePedimentoData
} from '$lib/api/dashboard/a76/pedimentos';
import type { PedimentoCode } from '$lib/api/dashboard/reference_data/pedimento_codes';
import type { CustomsSection } from '$lib/api/dashboard/reference_data/customs_sections';
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
import type { CodePedimentoRegimen } from '$lib/api/dashboard/reference_data/code_pedimento_regimens';
// Get sidebar context
const sidebar = useSidebar();
@@ -76,8 +80,9 @@
let activeTab = $state('general');
// Focus first input when switching main tabs (mouse or shortcut)
$effect(() => {
if (activeTab) {
if (activeTab && $interactionMode === 'keyboard') {
focusStore.request('first-input');
}
});