375 lines
12 KiB
Svelte
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}
|