Generacion de archivos winsaai
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
const BASE_URL = import.meta.env.VITE_API_URL || '';
|
||||
|
||||
export interface WinsaaiGenerationRequest {
|
||||
invoice_ids: number[];
|
||||
is_temporal: boolean;
|
||||
}
|
||||
|
||||
export interface WinsaaiResponse {
|
||||
task_id: string;
|
||||
status: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const reportsWinsaaiApi = {
|
||||
invoices: {
|
||||
triggerGeneration: async (payload: WinsaaiGenerationRequest): Promise<WinsaaiResponse> => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await fetch(`${BASE_URL}/v1/a76/reports/importacion/winsaai/invoices/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!response.ok) throw new Error('Error al iniciar la generación WINSAAI de facturas');
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
getTaskStatus: async (taskId: string): Promise<any> => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await fetch(`${BASE_URL}/v1/a76/reports/importacion/winsaai/invoices/status/${taskId}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (!response.ok) throw new Error('Error al consultar estado WINSAAI de facturas');
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
downloadFile: async (taskId: string): Promise<Response> => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
return await fetch(`${BASE_URL}/v1/a76/reports/importacion/winsaai/invoices/download/${taskId}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
pedimentos: {
|
||||
triggerGeneration: async (pedimentoIds: number[], isTemporal: boolean = true, isByClass: boolean = false): Promise<WinsaaiResponse> => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await fetch(`${BASE_URL}/v1/a76/reports/importacion/winsaai/pedimentos/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pedimento_ids: pedimentoIds,
|
||||
is_temporal: isTemporal,
|
||||
is_by_class: isByClass
|
||||
})
|
||||
});
|
||||
if (!response.ok) throw new Error('Error al iniciar la generación WINSAAI de pedimentos');
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
getTaskStatus: async (taskId: string): Promise<any> => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await fetch(`${BASE_URL}/v1/a76/reports/importacion/winsaai/pedimentos/status/${taskId}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (!response.ok) throw new Error('Error al consultar estado WINSAAI de pedimentos');
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
downloadFile: async (taskId: string): Promise<Response> => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
return await fetch(`${BASE_URL}/v1/a76/reports/importacion/winsaai/pedimentos/download/${taskId}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Dialog from "./index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import type { Snippet } from 'svelte';
|
||||
import Overlay from './dialog-overlay.svelte';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -19,13 +19,13 @@
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Portal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Portal {...portalProps}>
|
||||
<Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
@@ -33,11 +33,11 @@
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close
|
||||
class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute end-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
||||
class="absolute end-4 top-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</DialogPrimitive.Content>
|
||||
</Dialog.Portal>
|
||||
</DialogPrimitive.Portal>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Dialog } from "bits-ui";
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
import Title from "./dialog-title.svelte";
|
||||
import Footer from "./dialog-footer.svelte";
|
||||
@@ -9,8 +9,8 @@ import Description from "./dialog-description.svelte";
|
||||
import Trigger from "./dialog-trigger.svelte";
|
||||
import Close from "./dialog-close.svelte";
|
||||
|
||||
const Root = Dialog.Root;
|
||||
const Portal = Dialog.Portal;
|
||||
const Root = DialogPrimitive.Root;
|
||||
const Portal = DialogPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Dialog } from "bits-ui";
|
||||
const SheetPrimitive = Dialog;
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
const SheetPrimitive = DialogPrimitive;
|
||||
import Trigger from "./sheet-trigger.svelte";
|
||||
import Close from "./sheet-close.svelte";
|
||||
import Overlay from "./sheet-overlay.svelte";
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
import { consolidatedReportsApi } from '$lib/api/dashboard/a76/reports/reports-consolidated';
|
||||
import { dischargeReportsApi } from '$lib/api/dashboard/a76/reports/reports-descargo';
|
||||
import { avisoConsolidadoReportsApi } from '$lib/api/dashboard/a76/reports/reports-aviso-consolidado';
|
||||
import { reportsTransmissionApi } from '$lib/api/dashboard/a76/reports/reports-transmission';
|
||||
import { reportsWinsaaiApi } from '$lib/api/dashboard/a76/reports/reports-winsaai';
|
||||
import DataTable from '$lib/components/dashboard/invoices/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/invoices/columns.js';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
@@ -364,8 +367,10 @@
|
||||
|
||||
// Estado para el diálogo de progreso
|
||||
let showProgressDialog = $state(false);
|
||||
let isWinsaiiConfirmOpen = $state(false);
|
||||
let currentTaskId = $state<string | null>(null);
|
||||
let currentStatusFunction = $state<((taskId: string) => Promise<any>) | null>(null);
|
||||
let progressDialogTitle = $state('Generando documento');
|
||||
|
||||
// Utilidad para convertir Base64 a Blob
|
||||
function base64ToBlob(base64: string, type: string) {
|
||||
@@ -394,6 +399,7 @@
|
||||
// 2. Abrir diálogo de progreso
|
||||
currentTaskId = task_id;
|
||||
currentStatusFunction = invoicesReportsApi.getTaskStatus;
|
||||
progressDialogTitle = 'Generando PDF de Factura';
|
||||
showProgressDialog = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -417,6 +423,7 @@
|
||||
// 2. Abrir diálogo de progreso
|
||||
currentTaskId = task_id;
|
||||
currentStatusFunction = consolidatedReportsApi.getTaskStatus;
|
||||
progressDialogTitle = 'Generando Consolidado';
|
||||
showProgressDialog = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -450,6 +457,7 @@
|
||||
// 2. Abrir diálogo de progreso
|
||||
currentTaskId = task_id;
|
||||
currentStatusFunction = dischargeReportsApi.getTaskStatus;
|
||||
progressDialogTitle = 'Generando Reporte PEPS';
|
||||
showProgressDialog = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -473,6 +481,7 @@
|
||||
// 2. Abrir diálogo de progreso
|
||||
currentTaskId = task_id;
|
||||
currentStatusFunction = avisoConsolidadoReportsApi.getTaskStatus;
|
||||
progressDialogTitle = 'Generando Aviso Consolidado';
|
||||
showProgressDialog = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -497,6 +506,7 @@
|
||||
currentTaskId = task_id;
|
||||
// Use the specific status function for Packing List
|
||||
currentStatusFunction = invoicesReportsApi.getPackingListTaskStatus;
|
||||
progressDialogTitle = 'Generando Packing List';
|
||||
showProgressDialog = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -504,6 +514,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleInterfaceAgenteAduanal(invoice: any) {
|
||||
if (!companyStore.activeCompany) {
|
||||
toast.error('No hay empresa seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
if (invoice.operation_type !== 'imp') {
|
||||
toast.info('La interfaz rápida solo está disponible para facturas de Importación');
|
||||
return;
|
||||
}
|
||||
|
||||
// Abrir confirmación
|
||||
isWinsaiiConfirmOpen = true;
|
||||
}
|
||||
|
||||
async function confirmWinsaiiGeneration() {
|
||||
if (!selectedInvoice) return;
|
||||
isWinsaiiConfirmOpen = false;
|
||||
|
||||
try {
|
||||
const res = await reportsWinsaaiApi.invoices.triggerGeneration({
|
||||
invoice_ids: [selectedInvoice.id],
|
||||
is_temporal: selectedInvoice.invoice_type === 'TEM'
|
||||
});
|
||||
|
||||
if (res.task_id) {
|
||||
currentTaskId = res.task_id;
|
||||
// Use the specific status and download functions for WINSAAI
|
||||
currentStatusFunction = reportsWinsaaiApi.invoices.getTaskStatus;
|
||||
progressDialogTitle = 'Generando Reporte WINSAAI';
|
||||
showProgressDialog = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('No se pudo iniciar la generación de Interface Agente Aduanal');
|
||||
}
|
||||
}
|
||||
|
||||
function onPdfComplete(result: any) {
|
||||
// Esta función se llama cuando el diálogo reporta SUCCESS
|
||||
try {
|
||||
@@ -806,8 +854,26 @@
|
||||
getStatus={currentStatusFunction}
|
||||
onComplete={onPdfComplete}
|
||||
onClose={closeProgressDialog}
|
||||
title={progressDialogTitle}
|
||||
/>
|
||||
|
||||
<AlertDialog.Root bind:open={isWinsaiiConfirmOpen}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>Sistema de Control de Aduanas e Inventarios</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
A la Factura <strong>{selectedInvoice?.invoice_number}</strong> de tipo
|
||||
<strong>{selectedInvoice?.document_type}</strong> se le ha asignado el proceso Generación del
|
||||
Archivo WINSAAI. ¿Desea Continuar o Cancelar?
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel>Cancelar</AlertDialog.Cancel>
|
||||
<AlertDialog.Action onclick={confirmWinsaiiGeneration}>Continuar</AlertDialog.Action>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
<!-- Footer fijo con botones de acción -->
|
||||
<div
|
||||
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
|
||||
@@ -873,6 +939,16 @@
|
||||
Packing List
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => selectedInvoice && handleInterfaceAgenteAduanal(selectedInvoice)}
|
||||
disabled={!selectedInvoice}
|
||||
>
|
||||
<Send class="mr-2 h-4 w-4" />
|
||||
Interface Agente Aduanal
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
||||
@@ -11,11 +11,15 @@
|
||||
import type { PageData } from './$types';
|
||||
import { browser } from '$app/environment';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { Plus, Filter, Trash2, RefreshCw } from 'lucide-svelte';
|
||||
import { Edit } from 'lucide-svelte';
|
||||
import { Edit, Send, Plus, Filter, Trash2, RefreshCw } from 'lucide-svelte';
|
||||
import { obtenerAtajosListaPedimento } from '$lib/config/shortcuts/dashboard/pedimentos/list';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { goto } from '$app/navigation';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { reportsWinsaaiApi } from '$lib/api/dashboard/a76/reports/reports-winsaai';
|
||||
import PdfProgressDialog from '$lib/components/dashboard/invoices/pdf-progress-dialog.svelte';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
@@ -82,6 +86,16 @@
|
||||
let selectedId = $state<number | null>(null);
|
||||
let hasSelection = $derived(selectedId !== null);
|
||||
let showDeleteDialog = $state(false);
|
||||
let isWinsaiiConfirmOpen = $state(false);
|
||||
let isWinsaiiByClass = $state(false);
|
||||
|
||||
// Estado para diálogos de progreso
|
||||
let showProgressDialog = $state(false);
|
||||
let progressDialogTitle = $state('Procesando...');
|
||||
let currentTaskId = $state<string | null>(null);
|
||||
let currentStatusFunction = $state<any>(null);
|
||||
|
||||
let selectedPedimento = $derived(allItems.find((p) => p.id === selectedId) || null);
|
||||
|
||||
function handleRowClick(pedimento: Pedimento) {
|
||||
// Toggle: si ya está seleccionado, deseleccionar; si no, seleccionar
|
||||
@@ -130,6 +144,81 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleInterfaceAgenteAduanal() {
|
||||
if (!companyStore.activeCompany) {
|
||||
toast.error('No hay empresa seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedPedimento) {
|
||||
toast.error('Por favor selecciona un pedimento');
|
||||
return;
|
||||
}
|
||||
|
||||
// Abrir confirmación
|
||||
isWinsaiiConfirmOpen = true;
|
||||
}
|
||||
|
||||
async function confirmWinsaiiGeneration() {
|
||||
if (!selectedPedimento) return;
|
||||
isWinsaiiConfirmOpen = false;
|
||||
|
||||
try {
|
||||
const res = await reportsWinsaaiApi.pedimentos.triggerGeneration(
|
||||
[selectedPedimento.id],
|
||||
true, // isTemporal
|
||||
isWinsaiiByClass
|
||||
);
|
||||
|
||||
if (res.task_id) {
|
||||
currentTaskId = res.task_id;
|
||||
currentStatusFunction = reportsWinsaaiApi.pedimentos.getTaskStatus;
|
||||
progressDialogTitle = 'Generando Reporte WINSAAI Pedimentos';
|
||||
showProgressDialog = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('No se pudo iniciar la generación de Interface Agente Aduanal');
|
||||
}
|
||||
}
|
||||
|
||||
function onPdfComplete(result: any) {
|
||||
try {
|
||||
if (result.status === 'success') {
|
||||
const blob = base64ToBlob(result.content, result.media_type);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = result.file_name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
toast.success('Archivo descargado exitosamente');
|
||||
} else {
|
||||
toast.error('Hubo un error en la generación: ' + (result.message || 'Desconocido'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error al procesar descarga:', e);
|
||||
toast.error('Error al procesar el archivo descargado');
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
showProgressDialog = false;
|
||||
currentTaskId = null;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function base64ToBlob(base64: string, type: string) {
|
||||
const binStr = atob(base64);
|
||||
const len = binStr.length;
|
||||
const arr = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
arr[i] = binStr.charCodeAt(i);
|
||||
}
|
||||
return new Blob([arr], { type: type });
|
||||
}
|
||||
|
||||
// Keyboard Shortcuts
|
||||
useShortcuts(
|
||||
'Pedimentos',
|
||||
@@ -455,6 +544,15 @@
|
||||
<Trash2 size={16} class="mr-1" />
|
||||
Borrar
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={handleInterfaceAgenteAduanal}
|
||||
disabled={!hasSelection}
|
||||
>
|
||||
<Send class="mr-2 h-4 w-4" />
|
||||
Interface Agente Aduanal
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -475,3 +573,38 @@
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<AlertDialog.Root bind:open={isWinsaiiConfirmOpen}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>¿Generar Interface Agente Aduanal?</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
Se generará el reporte WINSAAI para el pedimento seleccionado.
|
||||
<div class="mt-4 flex items-center space-x-2">
|
||||
<Checkbox id="byClass" bind:checked={isWinsaiiByClass} />
|
||||
<Label
|
||||
for="byClass"
|
||||
class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Agrupar por clase (SCAF)
|
||||
</Label>
|
||||
</div>
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="outline" onclick={() => (isWinsaiiConfirmOpen = false)}>Cancelar</Button>
|
||||
<Button onclick={confirmWinsaiiGeneration}>Generar</Button>
|
||||
</div>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
{#if showProgressDialog && currentTaskId}
|
||||
<PdfProgressDialog
|
||||
bind:open={showProgressDialog}
|
||||
taskId={currentTaskId}
|
||||
title={progressDialogTitle}
|
||||
getStatus={currentStatusFunction}
|
||||
onClose={() => (showProgressDialog = false)}
|
||||
onComplete={onPdfComplete}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user