Merge branch 'feature/doda-endpoints-faltantes' into feature/visible-clicks-csv

This commit is contained in:
2026-04-29 08:32:07 -06:00
13 changed files with 1209 additions and 336 deletions

View File

@@ -478,6 +478,56 @@ export async function getDodaAltaStatus(
return api.get<DodaAltaStatusResponse>(`/v1/a76/doda/alta-status/${taskId}`);
}
export async function postDodaConsulta(
dodaId: number,
companyId: number,
variant: 'doda' | 'pita' = 'doda'
): Promise<ApiResponse<DodaAltaResponse>> {
const params = new URLSearchParams({
company_id: companyId.toString(),
variant,
});
return api.post<DodaAltaResponse>(`/v1/a76/doda/${dodaId}/consulta?${params}`, {});
}
export async function getDodaConsultaStatus(
taskId: string
): Promise<ApiResponse<DodaAltaStatusResponse>> {
return api.get<DodaAltaStatusResponse>(`/v1/a76/doda/consulta-status/${taskId}`);
}
export async function postDodaConsultaApply(
dodaId: number,
taskId: string,
companyId: number
): Promise<ApiResponse<Record<string, unknown>>> {
const params = new URLSearchParams({
company_id: companyId.toString(),
});
return api.post<Record<string, unknown>>(
`/v1/a76/doda/${dodaId}/consulta-apply/${taskId}?${params}`,
{}
);
}
export async function postDodaEliminar(
dodaId: number,
companyId: number,
variant: 'doda' | 'pita' = 'doda'
): Promise<ApiResponse<DodaAltaResponse>> {
const params = new URLSearchParams({
company_id: companyId.toString(),
variant,
});
return api.post<DodaAltaResponse>(`/v1/a76/doda/${dodaId}/eliminar?${params}`, {});
}
export async function getDodaEliminarStatus(
taskId: string
): Promise<ApiResponse<DodaAltaStatusResponse>> {
return api.get<DodaAltaStatusResponse>(`/v1/a76/doda/eliminar-status/${taskId}`);
}
export async function getDodaElegibilidad(
dodaId: number,
companyId: number,

View File

@@ -7,6 +7,7 @@
getDodaAltaStatus,
type DodaAltaStatusResponse
} from '$lib/api/dashboard/a76/general_catalogs/doda';
import type { ApiResponse } from '$lib/api';
import { m } from '$lib/i18n/messages';
let {
@@ -14,6 +15,9 @@
taskId,
dodaId,
variant = 'doda',
title = m['sidebar.doda_alta.progress_title'](),
description = 'Task ID',
getStatus = getDodaAltaStatus,
onComplete,
onCancel
}: {
@@ -21,6 +25,9 @@
taskId: string;
dodaId?: number;
variant?: 'doda' | 'pita';
title?: string;
description?: string;
getStatus?: (taskId: string) => Promise<ApiResponse<DodaAltaStatusResponse>>;
onComplete?: (result: DodaAltaStatusResponse) => void;
onCancel?: () => void;
} = $props();
@@ -84,7 +91,7 @@
if (!taskId || !pollingActive || pollInFlight) return;
pollInFlight = true;
try {
const res = await getDodaAltaStatus(taskId);
const res = await getStatus(taskId);
if (res.error) {
consecutivePollErrors += 1;
@@ -151,8 +158,8 @@
<Dialog.Root bind:open>
<Dialog.Content class="max-w-md">
<Dialog.Header>
<Dialog.Title>{m['sidebar.doda_alta.progress_title']()}</Dialog.Title>
<Dialog.Description>Alta {variantLabel} — Task ID: {taskId}</Dialog.Description>
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Description>{description} {variantLabel} — Task ID: {taskId}</Dialog.Description>
</Dialog.Header>
<div class="space-y-4 py-2">

View File

@@ -1,357 +1,538 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { createColumns } from '$lib/components/dashboard/general_catalogs/doda/columns';
import DodaFormModal from '$lib/components/dashboard/general_catalogs/doda/doda-form-modal.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaDoda } from '$lib/config/shortcuts/dashboard/general_catalogs/doda/list';
import { dodaApi, type Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
import { companyStore } from '$lib/stores/company.svelte';
import { currentUser, userHasPermission } from '$lib/auth';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { toast } from 'svelte-sonner';
import { m } from '$lib/i18n/messages';
import {
Plus,
RefreshCw,
Pencil,
Trash2,
Search,
RotateCcw,
Send,
Loader2,
FileSpreadsheet,
Table
} from 'lucide-svelte';
import * as Card from '$lib/components/ui/card';
import * as Select from '$lib/components/ui/select';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Separator } from '$lib/components/ui/separator';
import { companyStore } from '$lib/stores/company.svelte';
import { isPitaCustomsClearance } from '$lib/components/dashboard/general_catalogs/doda/doda-form-helpers';
let { data } = $props();
import DataTable from '$lib/components/dashboard/general_catalogs/doda/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/doda/columns';
import DodaProgressDialog from '$lib/components/dashboard/despacho/doda/doda-progress-dialog.svelte';
import DodaExportExcelDialog from '$lib/components/dashboard/despacho/doda/doda-export-excel-dialog.svelte';
import { applyOptimisticDelete } from '$lib/components/dashboard/general_catalogs/doda/delete-list-state';
let dialogOpen = $state(false);
let editingItem = $state<Doda | null>(null);
let error = $state<string | null>(data.error || null);
let status = $state<number>(data.status || 200);
import {
getDodas,
deleteDoda,
exportDodaPedimentosDetail,
postDodaAlta,
postDodaConsulta,
postDodaConsultaApply,
postDodaEliminar,
getDodaAltaStatus,
getDodaElegibilidad,
getDodaConsultaStatus,
getDodaEliminarStatus,
type DodaAltaStatusResponse,
type Doda
} from '$lib/api/dashboard/a76/general_catalogs/doda';
// Permisos
const canView = $derived(userHasPermission($currentUser, 'cat_doda.view'));
const canCreate = $derived(userHasPermission($currentUser, 'cat_doda.create'));
const canEdit = $derived(userHasPermission($currentUser, 'cat_doda.edit'));
const canDelete = $derived(userHasPermission($currentUser, 'cat_doda.delete'));
let { data } = $props();
const isError = $derived(!canView || status >= 400 || error);
let allDodas = $state<Doda[]>(data.dodas?.items || []);
let dodaPage = $state(data.dodas?.page || 1);
let dodaPageSize = $state(50);
let dodaTotal = $state(data.dodas?.total || 0);
let dodaLoading = $state(false);
let dodaHasMore = $derived(allDodas.length < dodaTotal);
// Atajos
useShortcuts(
'Lista DODA',
obtenerAtajosListaDoda({
manejarNuevo: () => {
if (!canCreate) return;
editingItem = null;
dialogOpen = true;
},
manejarActualizar: () => reloadData()
})
);
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') || ''
});
// Filtros
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>;
let dodaFilterTimeout: ReturnType<typeof setTimeout>;
let allItems = $state<Doda[]>(data.dodas?.items || []);
let currentPage = $state(data.dodas?.page || 1);
let pageSize = $state(data.dodas?.page_size || 50);
let totalItems = $state(data.dodas?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
let selectedIds = $state<(string | number)[]>([]);
const selectedItem = $derived(
selectedIds.length === 1
? allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null
: null
);
let selectedDodaIds = $state<(string | number)[]>([]);
const selectedDoda = $derived(
selectedDodaIds.length === 1
? allDodas.find((item) => String(item.id) === String(selectedDodaIds[0])) ?? null
: null
);
const altaVariant = $derived<'doda' | 'pita'>(
selectedDoda && isPitaCustomsClearance(selectedDoda.customs_clearance) ? 'pita' : 'doda'
);
$effect(() => {
if (data.dodas) {
allItems = data.dodas.items || [];
currentPage = data.dodas.page || 1;
totalItems = data.dodas.total || 0;
pageSize = data.dodas.page_size || pageSize;
}
});
let progressDialogOpen = $state(false);
let exportDialogOpen = $state(false);
let currentTaskId = $state('');
let currentVariant = $state<'doda' | 'pita'>('doda');
let progressMode = $state<'alta' | 'consulta' | 'eliminar'>('alta');
let altaLoading = $state(false);
let consultaLoading = $state(false);
let eliminarExternoLoading = $state(false);
let deleteLoading = $state(false);
let pedimentosExportLoading = $state(false);
const hasIntegration = $derived(!!(selectedDoda?.integration_number || '').trim());
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const response = await dodaApi.list(1, pageSize, companyStore.activeCompany.id, {
integration_number: filters.integration_number || undefined,
patent: filters.patent || undefined,
status: filters.status || undefined,
operation_type: filters.operation_type || undefined
});
const payload = response.data;
if (payload?.items) {
allItems = payload.items;
currentPage = payload.page || 1;
totalItems = payload.total;
}
} catch (err) {
error = 'Error aplicando filtros';
} finally {
loading = false;
}
$effect(() => {
if (data.dodas) {
allDodas = data.dodas.items || [];
dodaPage = data.dodas.page || 1;
dodaTotal = data.dodas.total || 0;
}
});
const url = new URL($page.url);
Object.entries(filters).forEach(([key, value]) => {
if (value) url.searchParams.set(key, value);
else url.searchParams.delete(key);
});
history.replaceState(history.state, '', url);
}, 500);
}
$effect(() => {
const _ = { ...filters };
clearTimeout(dodaFilterTimeout);
dodaFilterTimeout = setTimeout(() => reloadDodas(), 400);
});
function clearFilters() {
filters.integration_number = '';
filters.patent = '';
filters.status = '';
filters.operation_type = '';
handleSearch();
}
async function reloadDodas() {
if (!browser) return;
dodaLoading = true;
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
const active = Object.fromEntries(Object.entries(filters).filter(([, v]) => v !== ''));
const res = await getDodas(1, dodaPageSize, active, Number(companyId));
if (res.data) {
allDodas = res.data.items;
dodaPage = 1;
dodaTotal = res.data.total;
selectedDodaIds = [];
}
} catch {
if (allDodas.length > 0) toast.error('Error al recargar DODAs');
} finally {
dodaLoading = false;
}
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
try {
const response = await dodaApi.list(currentPage + 1, pageSize, companyStore.activeCompany.id, {
integration_number: filters.integration_number || undefined,
patent: filters.patent || undefined,
status: filters.status || undefined,
operation_type: filters.operation_type || undefined
});
const payload = response.data;
if (payload?.items) {
allItems = [...allItems, ...payload.items];
currentPage = payload.page || (currentPage + 1);
totalItems = payload.total;
}
} catch (err) {
error = 'Error cargando mas datos';
} finally {
loading = false;
}
}
async function loadMoreDodas() {
if (dodaLoading || !dodaHasMore) return;
dodaLoading = true;
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
const active = Object.fromEntries(Object.entries(filters).filter(([, v]) => v !== ''));
const res = await getDodas(dodaPage + 1, dodaPageSize, active, Number(companyId));
if (res.data) {
allDodas = [...allDodas, ...res.data.items];
dodaPage++;
dodaTotal = res.data.total;
}
} finally {
dodaLoading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const response = await dodaApi.list(1, pageSize, companyStore.activeCompany.id, {
integration_number: filters.integration_number || undefined,
patent: filters.patent || undefined,
status: filters.status || undefined,
operation_type: filters.operation_type || undefined
});
const payload = response.data;
if (payload?.items) {
allItems = payload.items;
currentPage = payload.page || 1;
totalItems = payload.total;
}
} catch (err) {
error = 'Error al recargar datos';
} finally {
loading = false;
}
}
function handleEdit() {
if (selectedDoda) {
void goto(`/dashboard/general_catalogs/doda?doda_id=${selectedDoda.id}`, { noScroll: true });
}
}
function handleSuccess() {
dialogOpen = false;
editingItem = null;
selectedIds = [];
reloadData();
}
async function handleDelete() {
if (deleteLoading) return;
if (!companyStore.activeCompany) {
toast.error(m['sidebar.doda_alta.delete_missing_company']());
return;
}
if (selectedDodaIds.length !== 1) {
toast.error(m['sidebar.doda_alta.delete_select_one']());
return;
}
if (!selectedDoda) {
toast.error(m['sidebar.doda_alta.delete_not_found']());
return;
}
if (!confirm(m['sidebar.doda_alta.confirm_delete']())) return;
deleteLoading = true;
try {
const deletedId = selectedDoda.id;
await deleteDoda(deletedId, companyStore.activeCompany.id);
toast.success(m['sidebar.doda_alta.delete_success']());
const next = applyOptimisticDelete(allDodas, dodaTotal, deletedId);
allDodas = next.items;
dodaTotal = next.total;
selectedDodaIds = [];
await reloadDodas();
} catch (e) {
const msg = e instanceof Error ? e.message : m['sidebar.doda_alta.delete_error']();
toast.error(msg);
} finally {
deleteLoading = false;
}
}
function handleNew() {
if (!canCreate) return;
const url = new URL($page.url);
url.searchParams.set('doda_id', 'new');
goto(url.toString(), { replaceState: true });
}
async function handleExportPedimentos() {
if (pedimentosExportLoading) return;
if (!companyStore.activeCompany) {
toast.error(m['sidebar.doda_alta.delete_missing_company']());
return;
}
if (selectedDodaIds.length !== 1 || !selectedDoda) {
toast.error(m['sidebar.doda_alta.delete_select_one']());
return;
}
pedimentosExportLoading = true;
try {
await exportDodaPedimentosDetail(selectedDoda.id, companyStore.activeCompany.id, 'xls');
toast.success(m['sidebar.doda_alta.export_pedimentos_success']());
} catch (e) {
const msg = e instanceof Error ? e.message : m['sidebar.doda_alta.export_pedimentos_error']();
toast.error(msg || m['sidebar.doda_alta.export_pedimentos_error']());
} finally {
pedimentosExportLoading = false;
}
}
function handleEdit() {
if (!selectedItem || !canEdit) return;
const url = new URL($page.url);
url.searchParams.set('doda_id', selectedItem.id.toString());
goto(url.toString(), { replaceState: true });
}
function clearFilters() {
filters = { integration_number: '', patent: '', status: '', operation_type: '' };
}
async function handleDelete() {
if (!selectedItem || !canDelete || !companyStore.activeCompany) return;
if (confirm('¿Eliminar este registro?')) {
try {
await dodaApi.delete(selectedItem.id, companyStore.activeCompany.id);
selectedIds = [];
reloadData();
} catch (err) {
error = 'Error al eliminar';
}
}
}
async function handleAlta() {
if (!selectedDoda || !companyStore.activeCompany) return;
const companyId = companyStore.activeCompany.id;
const dodaId = selectedDoda.id;
const variant = altaVariant;
altaLoading = true;
try {
const elig = await getDodaElegibilidad(dodaId, companyId, variant);
if (elig.error) {
toast.error(`Error al verificar elegibilidad: ${elig.error}`);
return;
}
if (elig.data && !elig.data.can_alta) {
const msgs = elig.data.reasons.map((r) => `• ${r.message}`).join('\n');
toast.error(msgs || m['sidebar.doda_alta.eligibility_error']());
return;
}
const resp = await postDodaAlta(dodaId, companyId, variant);
if (resp.error) {
toast.error(`Error al enviar alta: ${resp.error}`);
return;
}
currentTaskId = resp.data!.task_id;
currentVariant = variant;
progressDialogOpen = true;
} finally {
altaLoading = false;
}
}
async function handlePrint() {
if (!selectedItem || !companyStore.activeCompany) return;
try {
const { printDoda } = await import('$lib/api/dashboard/a76/general_catalogs/doda');
await printDoda(selectedItem.id, companyStore.activeCompany.id);
} catch (err) {
error = 'Error al imprimir PDF';
}
}
async function handleConsultar() {
if (!selectedDoda || !companyStore.activeCompany || consultaLoading) return;
if (!hasIntegration) {
toast.error('El DODA aún no está generado para consultar.');
return;
}
consultaLoading = true;
try {
const resp = await postDodaConsulta(selectedDoda.id, companyStore.activeCompany.id, altaVariant);
if (resp.error || !resp.data?.task_id) {
toast.error(resp.error || 'Error al enviar consulta DODA');
return;
}
progressMode = 'consulta';
currentTaskId = resp.data.task_id;
currentVariant = altaVariant;
progressDialogOpen = true;
} finally {
consultaLoading = false;
}
}
function handleRowClick(row: Doda) {
selectedIds = selectedIds.includes(row.id) ? [] : [row.id];
}
async function handleEliminarExterno() {
if (!selectedDoda || !companyStore.activeCompany || eliminarExternoLoading) return;
if (!hasIntegration) {
toast.error('El DODA aún no está generado para eliminar externamente.');
return;
}
if (!confirm('¿Deseas eliminar este DODA en el servicio externo para volver a editarlo?')) return;
eliminarExternoLoading = true;
try {
const resp = await postDodaEliminar(selectedDoda.id, companyStore.activeCompany.id, altaVariant);
if (resp.error || !resp.data?.task_id) {
toast.error(resp.error || 'Error al enviar eliminación DODA');
return;
}
progressMode = 'eliminar';
currentTaskId = resp.data.task_id;
currentVariant = altaVariant;
progressDialogOpen = true;
} finally {
eliminarExternoLoading = false;
}
}
function handleRowDoubleClick(row: Doda) {
if (canEdit) {
const url = new URL($page.url);
url.searchParams.set('doda_id', row.id.toString());
goto(url.toString(), { replaceState: true });
}
}
async function onProgressComplete(_result: DodaAltaStatusResponse) {
progressDialogOpen = false;
if (progressMode === 'consulta' && selectedDoda && companyStore.activeCompany) {
const applyResp = await postDodaConsultaApply(
selectedDoda.id,
currentTaskId,
companyStore.activeCompany.id
);
if (applyResp.error) {
toast.error(`Consulta completada, pero no se pudo aplicar al DODA: ${applyResp.error}`);
}
}
await reloadDodas();
if (progressMode === 'eliminar') {
toast.success('Eliminación DODA completada. El registro quedó editable nuevamente.');
} else if (progressMode === 'consulta') {
toast.success('Consulta DODA completada y aplicada al registro.');
} else {
toast.success(m['sidebar.doda_alta.progress_success']());
}
}
const columns = $derived(createColumns('es', handleSuccess, { canEdit, canDelete }));
const progressTitle = $derived(
progressMode === 'consulta'
? 'Consulta DODA'
: progressMode === 'eliminar'
? 'Eliminación DODA'
: m['sidebar.doda_alta.progress_title']()
);
const progressDescription = $derived(
progressMode === 'consulta'
? 'Consulta'
: progressMode === 'eliminar'
? 'Eliminación'
: 'Alta'
);
const progressStatusGetter = $derived(
progressMode === 'consulta'
? getDodaConsultaStatus
: progressMode === 'eliminar'
? getDodaEliminarStatus
: getDodaAltaStatus
);
</script>
<div class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]">
<div class="flex flex-none items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">DODA</h1>
<p class="text-muted-foreground">Catálogo de DODA</p>
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-2xl font-bold tracking-tight">{m['sidebar.doda_alta.title']()}</h1>
<p class="text-muted-foreground">{m['sidebar.doda_alta.subtitle']()}</p>
</div>
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" onclick={() => reloadDodas()} disabled={dodaLoading}>
<RefreshCw class="mr-2 h-4 w-4 {dodaLoading ? 'animate-spin' : ''}" />
{m['sidebar.doda_alta.refresh']()}
</Button>
<Button size="sm" onclick={() => goto('/dashboard/general_catalogs/doda?doda_id=new')}>
<Plus class="mr-2 h-4 w-4" />
{m['sidebar.doda_alta.action_new']()}
</Button>
</div>
</div>
<Card.Root class="flex min-h-0 flex-1 flex-col border bg-background">
<Card.Header>
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<Card.Title>{m['sidebar.doda_alta.table_title']()}</Card.Title>
<div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-[220px_180px_170px_170px_auto] xl:items-center">
<div class="relative">
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder={m['sidebar.doda_alta.filter_integration_number']()}
bind:value={filters.integration_number}
class="h-9 bg-card pl-9"
/>
</div>
<Input
placeholder={m['sidebar.doda_alta.filter_patent']()}
bind:value={filters.patent}
class="h-9 bg-card"
/>
<Select.Root
type="single"
value={filters.status}
onValueChange={(v) => (filters.status = v)}
>
<Select.Trigger class="h-9 w-full bg-card">
{filters.status || m['sidebar.doda_alta.filter_status']()}
</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>
<Select.Root
type="single"
value={filters.operation_type}
onValueChange={(v) => (filters.operation_type = v)}
>
<Select.Trigger class="h-9 w-full bg-card">
{filters.operation_type === 'I'
? 'Importación'
: filters.operation_type === 'E'
? 'Exportación'
: m['sidebar.doda_alta.filter_operation_type']()}
</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>
<Button variant="outline" size="sm" class="h-9" onclick={clearFilters}>
<RotateCcw class="mr-2 h-4 w-4" />
Limpiar
</Button>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="rounded-md border bg-background">
<DataTable
data={allDodas}
columns={createColumns()}
loading={dodaLoading}
hasMore={dodaHasMore}
loadMore={loadMoreDodas}
selectedId={selectedDodaIds.length === 1 ? selectedDodaIds[0] : null}
onRowClick={(row) => {
selectedDodaIds = selectedDodaIds.includes(row.id) ? [] : [row.id];
}}
onRowDoubleClick={(item) =>
goto(`/dashboard/general_catalogs/doda?doda_id=${item.id}`, { noScroll: true })}
/>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground pt-2">
Mostrando {allDodas.length} de {dodaTotal} registros
<span class="ml-2"></span>
<span class="ml-2">Filtros activos: {Object.values(filters).filter((v) => v !== '').length}</span>
</div>
<div class="h-20"></div>
<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 flex-wrap justify-end gap-2">
<Button
variant="outline"
size="sm"
onclick={handleEdit}
disabled={selectedDodaIds.length !== 1 || hasIntegration}
>
<Pencil size={16} class="mr-2" />
{m['sidebar.doda_alta.action_edit']()}
</Button>
<Button
size="sm"
variant="secondary"
onclick={handleConsultar}
disabled={selectedDodaIds.length !== 1 || !hasIntegration || consultaLoading}
>
{#if consultaLoading}
<Loader2 size={16} class="mr-2 animate-spin" />
{:else}
<Search size={16} class="mr-2" />
{/if}
Consultar DODA
</Button>
<Button
size="sm"
variant="secondary"
class="border border-amber-500/30 bg-amber-500/10 text-amber-700 hover:bg-amber-500/20"
onclick={handleEliminarExterno}
disabled={selectedDodaIds.length !== 1 || !hasIntegration || eliminarExternoLoading}
>
{#if eliminarExternoLoading}
<Loader2 size={16} class="mr-2 animate-spin" />
{:else}
<Trash2 size={16} class="mr-2" />
{/if}
Eliminar DODA
</Button>
<Button
variant="outline"
size="sm"
onclick={handleDelete}
disabled={selectedDodaIds.length !== 1 || deleteLoading}
class="text-destructive hover:bg-destructive/10 hover:text-destructive"
>
{#if deleteLoading}
<Loader2 size={16} class="mr-2 animate-spin" />
{:else}
<Trash2 size={16} class="mr-2" />
{/if}
{m['sidebar.doda_alta.action_delete']()}
</Button>
<Button variant="outline" size="sm" onclick={() => (exportDialogOpen = true)}>
<FileSpreadsheet size={16} class="mr-2" />
{m['sidebar.doda_alta.action_export_excel']()}
</Button>
<Button
size="sm"
variant="secondary"
class="border border-primary/25 bg-primary/10 text-primary hover:bg-primary/15"
onclick={handleExportPedimentos}
disabled={selectedDodaIds.length !== 1 || pedimentosExportLoading || !companyStore.activeCompany}
title={m['sidebar.doda_alta.action_export_pedimentos']()}
>
{#if pedimentosExportLoading}
<Loader2 size={16} class="mr-2 animate-spin" />
{:else}
<Table size={16} class="mr-2" />
{/if}
{m['sidebar.doda_alta.action_export_pedimentos']()}
</Button>
<Separator orientation="vertical" class="mx-1 h-8 hidden sm:block" />
<Button
size="sm"
onclick={handleAlta}
disabled={selectedDodaIds.length !== 1 || altaLoading || hasIntegration}
title={altaVariant === 'pita' ? 'PITA' : 'DODA'}
>
{#if altaLoading}
<Loader2 size={16} class="mr-2 animate-spin" />
{:else}
<Send size={16} class="mr-2" />
{/if}
{m['sidebar.doda_alta.action_generar']()}
</Button>
</div>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
</Button>
{#if !isError && canCreate}
<Button class="h-9" onclick={handleNew}>
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
</Button>
<DodaExportExcelDialog bind:open={exportDialogOpen} companyId={companyStore.activeCompany?.id} />
{#if progressDialogOpen}
<DodaProgressDialog
bind:open={progressDialogOpen}
taskId={currentTaskId}
dodaId={selectedDoda?.id}
variant={currentVariant}
title={progressTitle}
description={progressDescription}
getStatus={progressStatusGetter}
onComplete={onProgressComplete}
onCancel={() => (progressDialogOpen = false)}
/>
{/if}
</div>
</div>
{#if isError}
<ErrorState
status={!canView ? 403 : status}
error={!canView ? 'Permission denied: cat_doda.view' : error || ''}
onRetry={reloadData}
/>
{:else}
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
<Card.Header>
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<Card.Title>Listado de DODA</Card.Title>
<div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-[200px_140px_160px_160px_auto] xl:items-center">
<Input placeholder="Folio" bind:value={filters.integration_number} oninput={handleSearch} class="h-9 bg-card" />
<Input placeholder="Patente" bind:value={filters.patent} oninput={handleSearch} class="h-9 bg-card" />
<select
bind:value={filters.status}
onchange={handleSearch}
class="h-9 w-full rounded-md border border-input bg-card px-3 py-1 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<option value="">Estatus</option>
<option value="PENDIENTE">PENDIENTE</option>
<option value="GENERADO">GENERADO</option>
<option value="VALIDADO">VALIDADO</option>
<option value="ELIMINADO">ELIMINADO</option>
</select>
<select
bind:value={filters.operation_type}
onchange={handleSearch}
class="h-9 w-full rounded-md border border-input bg-card px-3 py-1 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<option value="">Operación</option>
<option value="I">I - Importación</option>
<option value="E">E - Exportación</option>
</select>
<Button variant="outline" size="sm" class="h-9" onclick={clearFilters}>
Limpiar
</Button>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
<div class="h-full overflow-hidden rounded-md border bg-background">
<InfiniteDataTable
data={allItems} {columns} {loading} {hasMore} {loadMore}
{selectedIds} onSelectedIdsChange={(ids) => (selectedIds = ids)}
onRowClick={handleRowClick}
onRowDoubleClick={handleRowDoubleClick}
/>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">
Mostrando {allItems.length} de {totalItems} registros
</div>
<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 selectedItem}
Seleccionado: <span class="font-medium text-foreground">{selectedItem.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}>
Actualizar
</Button>
{#if canEdit}
<Button variant="outline" size="sm" onclick={handleEdit} disabled={!selectedItem}>
<Pencil size={16} class="mr-2" /> Editar
</Button>
{/if}
{#if canDelete}
<Button variant="outline" size="sm" onclick={handleDelete} disabled={!selectedItem} class="text-destructive hover:bg-destructive/10">
<Trash2 size={16} class="mr-2" /> Eliminar
</Button>
{/if}
<Button variant="secondary" size="sm" onclick={handlePrint} disabled={!selectedItem}>
Imprimir
</Button>
</div>
</div>
</div>
</div>
{/if}
{#if $page.url.searchParams.get('doda_id')}
<DodaFormModal
dodaIdParam={$page.url.searchParams.get('doda_id')!}
onClose={() => {
const url = new URL($page.url);
url.searchParams.delete('doda_id');
goto(url.toString(), { replaceState: true });
reloadData();
}}
onCreatedNavigateTo={(newId) => {
const url = new URL($page.url);
url.searchParams.set('doda_id', String(newId));
goto(url.toString(), { replaceState: true });
}}
/>
{/if}
</div>