Files
plantillas-proyectos/frontend/src/routes/dashboard/despacho/doda/+page.svelte
2026-04-26 18:13:20 -06:00

375 lines
12 KiB
Svelte

<script lang="ts">
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
} 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';
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';
import {
getDodas,
deleteDoda,
postDodaAlta,
getDodaElegibilidad,
type Doda
} from '$lib/api/dashboard/a76/general_catalogs/doda';
let { data } = $props();
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);
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 dodaFilterTimeout: ReturnType<typeof setTimeout>;
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'
);
let progressDialogOpen = $state(false);
let exportDialogOpen = $state(false);
let currentTaskId = $state('');
let currentVariant = $state<'doda' | 'pita'>('doda');
let altaLoading = $state(false);
let deleteLoading = $state(false);
$effect(() => {
if (data.dodas) {
allDodas = data.dodas.items || [];
dodaPage = data.dodas.page || 1;
dodaTotal = data.dodas.total || 0;
}
});
$effect(() => {
const _ = { ...filters };
clearTimeout(dodaFilterTimeout);
dodaFilterTimeout = setTimeout(() => reloadDodas(), 400);
});
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 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;
}
}
function handleEdit() {
if (selectedDoda) {
void goto(`/dashboard/general_catalogs/doda?doda_id=${selectedDoda.id}`, { noScroll: true });
}
}
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 clearFilters() {
filters = { integration_number: '', patent: '', status: '', operation_type: '' };
}
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;
}
}
function onAltaComplete() {
progressDialogOpen = false;
reloadDodas();
toast.success(m['sidebar.doda_alta.progress_success']());
}
</script>
<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}
>
<Pencil size={16} class="mr-2" />
{m['sidebar.doda_alta.action_edit']()}
</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>
<Separator orientation="vertical" class="mx-1 h-8 hidden sm:block" />
<Button
size="sm"
onclick={handleAlta}
disabled={selectedDodaIds.length !== 1 || altaLoading}
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>
<DodaExportExcelDialog bind:open={exportDialogOpen} companyId={companyStore.activeCompany?.id} />
{#if progressDialogOpen}
<DodaProgressDialog
bind:open={progressDialogOpen}
taskId={currentTaskId}
dodaId={selectedDoda?.id}
variant={currentVariant}
onComplete={onAltaComplete}
onCancel={() => (progressDialogOpen = false)}
/>
{/if}