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