Merge branch 'feature/doda-endpoints-faltantes' into feature/visible-clicks-csv
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user