Se introdujo el sistema de consolidados
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL || '';
|
||||
|
||||
export const consolidatedReportsApi = {
|
||||
|
||||
triggerPdfGeneration: async (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const endpoint = `${BASE_URL}/v1/a76/reports/importacion/consolidados/${invoiceId}/download-async?${params.toString()}`;
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Error al iniciar la generación del consolidado');
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
getTaskStatus: async (taskId: string) => {
|
||||
const endpoint = `${BASE_URL}/v1/a76/reports/importacion/consolidados/tasks/${taskId}`;
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Error al consultar estado del consolidado');
|
||||
return await response.json();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import * as Table from "$lib/components/ui/table";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { Search, Loader2, Hash } from "lucide-svelte";
|
||||
import { getUSTariffFractions, type USTariffFraction } from "$lib/api/dashboard/a76/general_catalogs/us-tariff-fractions";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
// --- PROPS ---
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean,
|
||||
onSelect: (item: USTariffFraction) => void
|
||||
} = $props();
|
||||
|
||||
// --- ESTADO ---
|
||||
let items = $state<USTariffFraction[]>([]);
|
||||
let loading = $state(false);
|
||||
let searchTerm = $state("");
|
||||
let loaded = $state(false);
|
||||
|
||||
// Filtro local
|
||||
let filteredItems = $derived(
|
||||
items.filter(i =>
|
||||
(i.code || "").includes(searchTerm) ||
|
||||
(i.description || "").toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open && !loaded && companyStore.activeCompany?.id) {
|
||||
loadFractions();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadFractions() {
|
||||
if (!companyStore.activeCompany?.id) {
|
||||
toast.error("No hay empresa seleccionada");
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const response = await getUSTariffFractions(1, 1000, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
console.error("Error al cargar fracciones americanas:", response.error);
|
||||
toast.error(`Error: ${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data?.items) {
|
||||
items = response.data.items;
|
||||
loaded = true;
|
||||
} else {
|
||||
console.warn("No se encontraron fracciones americanas:", response);
|
||||
toast.info("No se encontraron fracciones americanas registradas");
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("Excepción cargando fracciones americanas:", e);
|
||||
toast.error(`Error de conexión: ${e.message || e}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(item: USTariffFraction) {
|
||||
if (onSelect) onSelect(item);
|
||||
open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open={open}>
|
||||
<Dialog.Content class="sm:max-w-[800px] max-h-[80vh] flex flex-col">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Seleccionar Fracción Americana</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Seleccione la fracción arancelaria (HTS) del catálogo.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="relative w-full my-2">
|
||||
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Buscar por código o descripción..."
|
||||
class="pl-9"
|
||||
bind:value={searchTerm}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto border rounded-md min-h-[300px]">
|
||||
{#if loading}
|
||||
<div class="flex flex-col items-center justify-center h-48 gap-2 text-muted-foreground">
|
||||
<Loader2 class="h-8 w-8 animate-spin text-primary" />
|
||||
<p>Cargando catálogo...</p>
|
||||
</div>
|
||||
{:else if filteredItems.length === 0}
|
||||
<div class="flex flex-col items-center justify-center h-48 text-muted-foreground">
|
||||
<p>No se encontraron fracciones.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head class="w-[150px]">Código (HTS)</Table.Head>
|
||||
<Table.Head>Descripción</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each filteredItems as item}
|
||||
<Table.Row
|
||||
class="cursor-pointer hover:bg-accent/50 transition-colors"
|
||||
onclick={() => handleSelect(item)}
|
||||
>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Hash class="h-3 w-3 text-blue-500" />
|
||||
<span class="font-mono font-bold text-blue-600 dark:text-blue-400">
|
||||
{item.code}
|
||||
</span>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-medium text-sm">
|
||||
{item.description || '-'}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<div class="text-xs text-muted-foreground self-center mr-auto">
|
||||
{filteredItems.length} registros encontrados
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => open = false}>Cancelar</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -36,6 +36,7 @@
|
||||
import CurrencySelectorDialog from '$lib/components/dashboard/goods/modales/currency-selector-dialog.svelte';
|
||||
import CountrySelectorDialog from '$lib/components/dashboard/goods/modales/country-selector-dialog.svelte';
|
||||
import FractionSelectorDialog from '$lib/components/dashboard/goods/modales/fraction-selector-dialog.svelte';
|
||||
import USFractionSelectorDialog from '$lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte';
|
||||
|
||||
// --- PROPS ---
|
||||
let { partId = null, formType = 'inv' }: { partId?: number | null, formType?: 'inv' | 'fa' } = $props();
|
||||
@@ -57,6 +58,7 @@
|
||||
let showCurrencyModal = $state(false);
|
||||
let showCountryModal = $state(false);
|
||||
let showFractionModal = $state(false);
|
||||
let showUSFractionModal = $state(false);
|
||||
|
||||
// Descripciones Visuales
|
||||
let selectedClientName = $state("");
|
||||
@@ -222,6 +224,7 @@
|
||||
selectedCountryName = country.description_es;
|
||||
}
|
||||
function handleFractionSelect(item: any) { formData.fraction = item.fraction; }
|
||||
function handleUSFractionSelect(item: any) { formData.us_fraction = item.code; }
|
||||
|
||||
// --- SUBMIT ---
|
||||
async function handleSubmit() {
|
||||
@@ -436,7 +439,10 @@
|
||||
</div>
|
||||
<div class="md:col-span-6 space-y-2">
|
||||
<Label for="fa_us_fraction">Fracción Americana</Label>
|
||||
<Input id="fa_us_fraction" bind:value={formData.us_fraction} maxlength={10} class="font-mono" placeholder="HTS Code"/>
|
||||
<div class="flex gap-2">
|
||||
<Input id="fa_us_fraction" bind:value={formData.us_fraction} maxlength={10} class="font-mono cursor-pointer" placeholder="HTS Code" readonly onclick={() => showUSFractionModal = true}/>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showUSFractionModal = true} class="shrink-0"><FolderSearch class="h-4 w-4" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-6 space-y-2">
|
||||
<Label for="fa_sector">Sector</Label>
|
||||
@@ -691,7 +697,10 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-6 pt-4 border-t">
|
||||
<div class="md:col-span-6 space-y-2">
|
||||
<Label for="frac_us">Fracción Americana (HTS)</Label>
|
||||
<Input id="frac_us" bind:value={formData.us_fraction} maxlength={16} placeholder="Ej: 8501.10.00" class="font-mono"/>
|
||||
<div class="flex gap-2">
|
||||
<Input id="frac_us" bind:value={formData.us_fraction} maxlength={16} placeholder="Ej: 8501.10.00" class="font-mono cursor-pointer" readonly onclick={() => showUSFractionModal = true}/>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showUSFractionModal = true} class="shrink-0"><FolderSearch class="h-4 w-4" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -885,6 +894,7 @@
|
||||
<CurrencySelectorDialog bind:open={showCurrencyModal} onSelect={handleCurrencySelect} />
|
||||
<CountrySelectorDialog bind:open={showCountryModal} onSelect={handleCountrySelect} />
|
||||
<FractionSelectorDialog bind:open={showFractionModal} onSelect={handleFractionSelect} />
|
||||
<USFractionSelectorDialog bind:open={showUSFractionModal} onSelect={handleUSFractionSelect} />
|
||||
|
||||
<style>
|
||||
:global(.required::after) {
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
export let onClose: () => void;
|
||||
export let onComplete: (result: any) => void;
|
||||
|
||||
export let getStatus: ((taskId: string) => Promise<any>) | null = null;
|
||||
|
||||
let progress = 0;
|
||||
let statusMessage = "Iniciando...";
|
||||
let pollingInterval: any = null;
|
||||
@@ -42,7 +44,8 @@
|
||||
if (!taskId) return;
|
||||
|
||||
try {
|
||||
const response = await invoicesReportsApi.getTaskStatus(taskId);
|
||||
const apiCall = getStatus || invoicesReportsApi.getTaskStatus;
|
||||
const response = await apiCall(taskId);
|
||||
|
||||
if (response.state === 'PROCESSING' && response.info) {
|
||||
progress = response.info.current || 0;
|
||||
@@ -118,7 +121,7 @@
|
||||
|
||||
<Dialog.Footer>
|
||||
{#if hasError}
|
||||
<Button variant="secondary" on:click={onClose}>Cerrar</Button>
|
||||
<Button variant="secondary" onclick={onClose}>Cerrar</Button>
|
||||
{/if}
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
|
||||
@@ -40,15 +40,15 @@ class CompanyStore {
|
||||
if (preloadedCompanies && preloadedCompanies.length > 0) {
|
||||
// Detectar si el tenant ha cambiado
|
||||
const newTenantId = preloadedCompanies[0].tenant_id;
|
||||
|
||||
|
||||
// Si el tenant cambió, limpiar el store primero
|
||||
if (this._currentTenantId !== null && this._currentTenantId !== newTenantId) {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
|
||||
this._currentTenantId = newTenantId;
|
||||
this._companies = preloadedCompanies;
|
||||
|
||||
|
||||
// Si hay compañías y no hay una activa, seleccionar la primera o la guardada
|
||||
if (this._companies.length > 0 && !this._activeCompany) {
|
||||
// Intentar restaurar la compañía guardada
|
||||
@@ -67,13 +67,13 @@ class CompanyStore {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Si no hay datos pre-cargados, hacer fetch (fallback)
|
||||
// Solo en el navegador, nunca durante SSR
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this._loading = true;
|
||||
try {
|
||||
const response = await fetch('/api/v1/a76/company/my-companies', {
|
||||
@@ -81,22 +81,22 @@ class CompanyStore {
|
||||
});
|
||||
if (response.ok) {
|
||||
const newCompanies = await response.json();
|
||||
|
||||
|
||||
// Detectar si el tenant ha cambiado
|
||||
if (newCompanies.length > 0) {
|
||||
const newTenantId = newCompanies[0].tenant_id;
|
||||
|
||||
|
||||
// Si el tenant cambió, limpiar el store primero
|
||||
if (this._currentTenantId !== null && this._currentTenantId !== newTenantId) {
|
||||
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
|
||||
this._currentTenantId = newTenantId;
|
||||
}
|
||||
|
||||
|
||||
this._companies = newCompanies;
|
||||
|
||||
|
||||
// Si hay compañías y no hay una activa, seleccionar la primera
|
||||
if (this._companies.length > 0 && !this._activeCompany) {
|
||||
this.setActiveCompany(this._companies[0], true); // silent=true para inicialización
|
||||
@@ -123,23 +123,23 @@ class CompanyStore {
|
||||
setActiveCompany(company: Company, silent: boolean = false) {
|
||||
const previousCompanyId = this._activeCompany?.id;
|
||||
this._activeCompany = company;
|
||||
|
||||
|
||||
// Guardar en localStorage para persistencia
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('activeCompanyId', company.id.toString());
|
||||
}
|
||||
|
||||
|
||||
// Guardar en cookie para acceso desde el servidor (SSR)
|
||||
if (typeof document !== 'undefined') {
|
||||
document.cookie = `active_company_id=${company.id}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`;
|
||||
}
|
||||
|
||||
|
||||
// Despachar evento personalizado solo si:
|
||||
// 1. No es silent (no es inicialización)
|
||||
// 2. Y realmente cambió la compañía (el ID es diferente)
|
||||
if (!silent && typeof window !== 'undefined' && previousCompanyId !== company.id) {
|
||||
window.dispatchEvent(new CustomEvent('companyChanged', {
|
||||
detail: { companyId: company.id }
|
||||
window.dispatchEvent(new CustomEvent('companyChanged', {
|
||||
detail: { companyId: company.id }
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -167,12 +167,12 @@ class CompanyStore {
|
||||
this._companies = [];
|
||||
this._loading = false;
|
||||
this._currentTenantId = null;
|
||||
|
||||
|
||||
// Limpiar localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('activeCompanyId');
|
||||
}
|
||||
|
||||
|
||||
// Limpiar cookie
|
||||
if (typeof document !== 'undefined') {
|
||||
document.cookie = 'active_company_id=; path=/; max-age=0';
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { invoicesApi, type Invoice, type OperationType } from '$lib/api/dashboard/a76/invoices';
|
||||
import { invoicesReportsApi } from '$lib/api/dashboard/a76/reports/reports-invoices';
|
||||
import { consolidatedReportsApi } from '$lib/api/dashboard/a76/reports/reports-consolidated';
|
||||
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';
|
||||
@@ -12,7 +13,7 @@
|
||||
import type { PageData } from './$types';
|
||||
import { browser } from '$app/environment';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { Plus, RefreshCw, FileDown, RotateCcw } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw, FileText, RotateCcw, Boxes } from 'lucide-svelte';
|
||||
|
||||
// IMPORTANTE: Asegúrate de tener instalada svelte-sonner para las notificaciones
|
||||
import { toast } from "svelte-sonner";
|
||||
@@ -334,6 +335,7 @@
|
||||
// Estado para el diálogo de progreso
|
||||
let showProgressDialog = $state(false);
|
||||
let currentTaskId = $state<string | null>(null);
|
||||
let currentStatusFunction = $state<((taskId: string) => Promise<any>) | null>(null);
|
||||
|
||||
// Utilidad para convertir Base64 a Blob
|
||||
function base64ToBlob(base64: string, type: string) {
|
||||
@@ -361,6 +363,7 @@
|
||||
|
||||
// 2. Abrir diálogo de progreso
|
||||
currentTaskId = task_id;
|
||||
currentStatusFunction = invoicesReportsApi.getTaskStatus;
|
||||
showProgressDialog = true;
|
||||
|
||||
} catch (error) {
|
||||
@@ -369,6 +372,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDownloadConsolidated(invoice: any) {
|
||||
if (!companyStore.activeCompany) {
|
||||
toast.error("No hay empresa seleccionada");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Trigger: Iniciar la tarea en Celery (Consolidado)
|
||||
const { task_id } = await consolidatedReportsApi.triggerPdfGeneration(
|
||||
invoice.id,
|
||||
companyStore.activeCompany.id
|
||||
);
|
||||
|
||||
// 2. Abrir diálogo de progreso
|
||||
currentTaskId = task_id;
|
||||
currentStatusFunction = consolidatedReportsApi.getTaskStatus;
|
||||
showProgressDialog = true;
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("No se pudo iniciar la descarga del consolidado");
|
||||
}
|
||||
}
|
||||
|
||||
function onPdfComplete(result: any) {
|
||||
// Esta función se llama cuando el diálogo reporta SUCCESS
|
||||
try {
|
||||
@@ -454,6 +481,7 @@
|
||||
function closeProgressDialog() {
|
||||
showProgressDialog = false;
|
||||
currentTaskId = null;
|
||||
currentStatusFunction = null;
|
||||
}
|
||||
|
||||
// --- AQUÍ PASAMOS LA FUNCIÓN DE DESCARGA A LAS COLUMNAS ---
|
||||
@@ -579,6 +607,7 @@
|
||||
<PdfProgressDialog
|
||||
bind:open={showProgressDialog}
|
||||
taskId={currentTaskId}
|
||||
getStatus={currentStatusFunction}
|
||||
onComplete={onPdfComplete}
|
||||
onClose={closeProgressDialog}
|
||||
/>
|
||||
@@ -597,8 +626,12 @@
|
||||
Desactualizar
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onclick={() => selectedInvoice && handleDownloadPdf(selectedInvoice)} disabled={!selectedInvoice}>
|
||||
<FileDown class="h-4 w-4 mr-2" />
|
||||
Descargar PDF
|
||||
<FileText class="h-4 w-4 mr-2" />
|
||||
Factura
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onclick={() => selectedInvoice && handleDownloadConsolidated(selectedInvoice)} disabled={!selectedInvoice}>
|
||||
<Boxes class="h-4 w-4 mr-2" />
|
||||
Consolidado
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user