Se mejoro el disenio de partes, se genero la informacion mas precisa en los reportes y se carga el logo en las instanacias de las empresas
This commit is contained in:
@@ -11,9 +11,11 @@
|
||||
createCompany,
|
||||
updateCompany,
|
||||
getCompany, // Asumiendo que esta función existe en tu API
|
||||
uploadCompanyLogo,
|
||||
type Company
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/company';
|
||||
import { ArrowLeft, LoaderCircle, Save } from 'lucide-svelte';
|
||||
import { ArrowLeft, LoaderCircle, Save, Upload } from 'lucide-svelte';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
// 1. Lógica de Navegación y Modo
|
||||
const id = $derived($page.params.id);
|
||||
@@ -21,9 +23,10 @@
|
||||
const title = $derived(isEdit ? "Editar Empresa" : "Nueva Empresa");
|
||||
|
||||
let loading = $state(false);
|
||||
let uploading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// 2. Estado Inicial (Reset)
|
||||
// ... (Initial Data) ...
|
||||
const initialData = {
|
||||
name: '',
|
||||
rfc: '',
|
||||
@@ -43,12 +46,20 @@
|
||||
is_service_company: false,
|
||||
order_format_type: '',
|
||||
ctpat_svi: '',
|
||||
trusted_exporter_number: ''
|
||||
trusted_exporter_number: '',
|
||||
logo: '',
|
||||
previous_code: 0,
|
||||
client_name: '',
|
||||
subassembly_mode: '',
|
||||
broker_company: '',
|
||||
inter_db_name: '',
|
||||
prevalidator_key: '',
|
||||
seventh_amendment: false
|
||||
};
|
||||
|
||||
let formData = $state({ ...initialData });
|
||||
|
||||
// 3. Efecto para "Heredar" datos o Limpiar
|
||||
// ... (Fetch Data) ...
|
||||
$effect(() => {
|
||||
if (isEdit) {
|
||||
fetchData(id);
|
||||
@@ -61,7 +72,6 @@
|
||||
async function fetchData(companyId: string) {
|
||||
loading = true;
|
||||
try {
|
||||
// Nota: Aquí usamos tu API para traer la info de una sola empresa
|
||||
const response = await getCompany(Number(companyId));
|
||||
if (response.data) {
|
||||
const item = response.data;
|
||||
@@ -84,7 +94,15 @@
|
||||
is_service_company: item.is_service_company || false,
|
||||
order_format_type: item.order_format_type || '',
|
||||
ctpat_svi: item.ctpat_svi || '',
|
||||
trusted_exporter_number: item.trusted_exporter_number || ''
|
||||
trusted_exporter_number: item.trusted_exporter_number || '',
|
||||
logo: item.logo || '',
|
||||
previous_code: item.previous_code || 0,
|
||||
client_name: item.client_name || '',
|
||||
subassembly_mode: item.subassembly_mode || '',
|
||||
broker_company: item.broker_company || '',
|
||||
inter_db_name: item.inter_db_name || '',
|
||||
prevalidator_key: item.prevalidator_key || '',
|
||||
seventh_amendment: item.seventh_amendment || false
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -96,6 +114,31 @@
|
||||
|
||||
const clean = (value: any) => (typeof value === 'string' ? value.trim() || null : value);
|
||||
|
||||
async function handleFileSelect(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (!input.files || input.files.length === 0) return;
|
||||
|
||||
const file = input.files[0];
|
||||
if (!isEdit) {
|
||||
alert("Primero debes guardar la empresa antes de subir un logo.");
|
||||
return;
|
||||
}
|
||||
|
||||
uploading = true;
|
||||
try {
|
||||
const res = await uploadCompanyLogo(Number(id), file);
|
||||
if (res.data) {
|
||||
formData.logo = res.data.path;
|
||||
} else if (res.error) {
|
||||
alert("Error al subir imagen: " + res.error);
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Error al intentar subir la imagen");
|
||||
} finally {
|
||||
uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
@@ -122,7 +165,15 @@
|
||||
manufacturer_id: clean(formData.manufacturer_id),
|
||||
order_format_type: clean(formData.order_format_type),
|
||||
ctpat_svi: clean(formData.ctpat_svi),
|
||||
trusted_exporter_number: clean(formData.trusted_exporter_number)
|
||||
trusted_exporter_number: clean(formData.trusted_exporter_number),
|
||||
logo: clean(formData.logo),
|
||||
previous_code: Number(formData.previous_code) || 0,
|
||||
client_name: clean(formData.client_name),
|
||||
subassembly_mode: clean(formData.subassembly_mode),
|
||||
broker_company: clean(formData.broker_company),
|
||||
inter_db_name: clean(formData.inter_db_name),
|
||||
prevalidator_key: clean(formData.prevalidator_key),
|
||||
seventh_amendment: formData.seventh_amendment
|
||||
};
|
||||
|
||||
const response = isEdit
|
||||
@@ -131,6 +182,27 @@
|
||||
|
||||
if (response.error) throw new Error(response.error);
|
||||
|
||||
// Update global store if we are editing the active company
|
||||
if (response.data) {
|
||||
const updatedComp = response.data;
|
||||
// We verify if we are editing the currently active company
|
||||
if (companyStore.activeCompany?.id === updatedComp.id) {
|
||||
// We update the store.
|
||||
// IMPORTANT: To force image refresh, we might need a cache buster in the sidebar,
|
||||
// but updating the store object is Step 1.
|
||||
companyStore.setActiveCompany({
|
||||
id: updatedComp.id,
|
||||
name: updatedComp.name || '',
|
||||
rfc: updatedComp.rfc || '',
|
||||
logo: updatedComp.logo || '',
|
||||
tenant_id: updatedComp.tenant_id
|
||||
});
|
||||
|
||||
// Force reload of company list to ensure integrity
|
||||
companyStore.loadCompanies();
|
||||
}
|
||||
}
|
||||
|
||||
goto('/dashboard/general_catalogs/company_information');
|
||||
} catch (e: any) {
|
||||
error = e.message || 'Error al guardar';
|
||||
@@ -181,6 +253,37 @@
|
||||
<Label for="main_activity">Actividad Principal</Label>
|
||||
<Input id="main_activity" bind:value={formData.main_activity} />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="logo">Ruta del Logo</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input id="logo" bind:value={formData.logo} placeholder="/path/to/logo.png" />
|
||||
{#if isEdit}
|
||||
<div class="relative">
|
||||
<Button variant="outline" size="icon" disabled={uploading}>
|
||||
{#if uploading}
|
||||
<LoaderCircle class="h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Upload class="h-4 w-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="absolute inset-0 opacity-0 cursor-pointer"
|
||||
onchange={handleFileSelect}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-[0.8rem] text-muted-foreground">Sube una imagen para obtener su ruta local.</p>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="client_name">Nombre Cliente (Maquila)</Label>
|
||||
<Input id="client_name" bind:value={formData.client_name} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="programas" class="space-y-4 pt-4">
|
||||
@@ -204,6 +307,16 @@
|
||||
<Input id="prosec_auth" bind:value={formData.prosec_authorization} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="broker_company">Empresa Broker</Label>
|
||||
<Input id="broker_company" bind:value={formData.broker_company} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="subassembly">Modo Sub-ensamble</Label>
|
||||
<Input id="subassembly" bind:value={formData.subassembly_mode} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="responsable" class="space-y-4 pt-4">
|
||||
@@ -243,8 +356,24 @@
|
||||
<Switch id="service" bind:checked={formData.is_service_company} />
|
||||
<Label for="service">Es Empresa de Servicios</Label>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 p-4 border rounded-lg">
|
||||
<Switch id="seventh" bind:checked={formData.seventh_amendment} />
|
||||
<Label for="seventh">Séptima Enmienda</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 pt-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="previous_code">Código Anterior</Label>
|
||||
<Input id="previous_code" type="number" bind:value={formData.previous_code} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="inter_db">Base de Datos Intermedia</Label>
|
||||
<Input id="inter_db" bind:value={formData.inter_db_name} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="prevalidator">Clave Prevalidador</Label>
|
||||
<Input id="prevalidator" bind:value={formData.prevalidator_key} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="man_id">Manufacturer ID (MID)</Label>
|
||||
<Input id="man_id" bind:value={formData.manufacturer_id} />
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
import { Plus, RefreshCw, Package } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { partsApi, type Part } from '$lib/api/dashboard/a76/parts';
|
||||
import { clientsProvidersApi } from '$lib/api/dashboard/a76/clients-providers';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
// Estado de la lista de partes
|
||||
let parts = $state<Part[]>([]);
|
||||
let clientsMap = $state<Record<number, string>>({}); // Mapa ID -> Nombre
|
||||
let selectedPart = $state<Part | null>(null);
|
||||
let isLoading = $state(false);
|
||||
let searchPartNumber = $state('');
|
||||
@@ -28,9 +30,11 @@
|
||||
(p.description_spanish?.toLowerCase().includes(searchDescription.toLowerCase()) ?? false) ||
|
||||
(p.description_english?.toLowerCase().includes(searchDescription.toLowerCase()) ?? false);
|
||||
|
||||
// Filtro por cliente
|
||||
// Filtro por cliente (Busca en nombre o ID)
|
||||
const clientName = clientsMap[p.client_id] || '';
|
||||
const matchesClient = !searchClient ||
|
||||
(p.client_id?.toString().includes(searchClient) ?? false);
|
||||
(p.client_id?.toString().includes(searchClient) ?? false) ||
|
||||
clientName.toLowerCase().includes(searchClient.toLowerCase());
|
||||
|
||||
// Filtro por clase
|
||||
const matchesClass = !searchClass ||
|
||||
@@ -44,10 +48,40 @@
|
||||
$effect(() => {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (companyId) {
|
||||
loadParts();
|
||||
loadData();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadData() {
|
||||
await Promise.all([loadParts(), loadClients()]);
|
||||
}
|
||||
|
||||
async function loadClients() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
|
||||
try {
|
||||
// Fetch all clients/providers to ensure we map "both" types as well
|
||||
const response = await clientsProvidersApi.list(
|
||||
companyId,
|
||||
1,
|
||||
1000
|
||||
);
|
||||
|
||||
const data = (response as any).data || response;
|
||||
const items = data.items || [];
|
||||
|
||||
const map: Record<number, string> = {};
|
||||
items.forEach((c: any) => {
|
||||
map[c.id] = c.name;
|
||||
});
|
||||
clientsMap = map;
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error cargando clientes:", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadParts() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
@@ -78,17 +112,35 @@
|
||||
}
|
||||
|
||||
async function handleRefresh() {
|
||||
await loadParts();
|
||||
await loadData();
|
||||
toast.success('Partes actualizadas');
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
|
||||
async function handleDelete() {
|
||||
if (!selectedPart) {
|
||||
toast.error('Selecciona una parte para borrar');
|
||||
return;
|
||||
}
|
||||
// TODO: Implementar eliminación
|
||||
toast.info('Función de eliminación pendiente');
|
||||
|
||||
const confirmed = window.confirm(`¿Estás seguro de que deseas eliminar la parte ${selectedPart.part_number}? Esta acción no se puede deshacer.`);
|
||||
if (!confirmed) return;
|
||||
|
||||
isLoading = true;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
|
||||
await partsApi.delete(selectedPart.id, companyId);
|
||||
toast.success('Parte eliminada exitosamente');
|
||||
selectedPart = null;
|
||||
await loadData();
|
||||
} catch (e) {
|
||||
console.error("Error al eliminar:", e);
|
||||
toast.error('Error al eliminar la parte');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -135,7 +187,7 @@
|
||||
<Label class="text-xs">Cliente</Label>
|
||||
<Input
|
||||
bind:value={searchClient}
|
||||
placeholder="ID de cliente..."
|
||||
placeholder="Nombre o ID..."
|
||||
class="h-9"
|
||||
/>
|
||||
</div>
|
||||
@@ -215,7 +267,12 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-2 py-1 font-medium text-sm">{part.description_spanish || ''}</td>
|
||||
<td class="px-2 py-1 text-muted-foreground">{part.client_id || '-'}</td>
|
||||
<td class="px-2 py-1 text-muted-foreground">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-xs">{clientsMap[part.client_id] || 'Cargando...'}</span>
|
||||
<span class="text-[10px] opacity-70">ID: {part.client_id}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-2 py-1">
|
||||
{#if part.part_class}
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400">
|
||||
@@ -263,7 +320,7 @@
|
||||
<Label class="text-[10px] uppercase text-muted-foreground font-bold">Cliente</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Package class="h-3 w-3 text-blue-500" />
|
||||
<span class="text-sm font-bold">{selectedPart.client_id || '-'}</span>
|
||||
<span class="text-sm font-bold">{clientsMap[selectedPart.client_id] || selectedPart.client_id}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
import type { PageData } from './$types';
|
||||
import { browser } from '$app/environment';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw, FileDown, RotateCcw } from 'lucide-svelte';
|
||||
|
||||
// IMPORTANTE: Asegúrate de tener instalada svelte-sonner para las notificaciones
|
||||
import { toast } from "svelte-sonner";
|
||||
import PdfProgressDialog from '$lib/components/dashboard/invoices/pdf-progress-dialog.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
@@ -151,6 +152,24 @@
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Estado para selección de fila
|
||||
let selectedInvoiceId = $state<number | null>(null);
|
||||
|
||||
function handleRowClick(invoice: Invoice) {
|
||||
// Si ya está seleccionado, lo deseleccionamos (opcional, si queremos permitir toggle)
|
||||
// O simplemente lo seleccionamos. Aquí implemento toggle.
|
||||
if (selectedInvoiceId === invoice.id) {
|
||||
selectedInvoiceId = null;
|
||||
} else {
|
||||
selectedInvoiceId = invoice.id;
|
||||
}
|
||||
console.log('Selected Invoice ID:', selectedInvoiceId);
|
||||
}
|
||||
|
||||
const selectedInvoice = $derived(
|
||||
selectedInvoiceId ? allItems.find(i => i.id === selectedInvoiceId) : null
|
||||
);
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
@@ -312,83 +331,72 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Estado para el diálogo de progreso
|
||||
let showProgressDialog = $state(false);
|
||||
let currentTaskId = $state<string | null>(null);
|
||||
|
||||
// Utilidad para convertir Base64 a Blob
|
||||
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);
|
||||
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 });
|
||||
}
|
||||
return new Blob([arr], { type: type });
|
||||
}
|
||||
|
||||
async function handleDownloadPdf(invoice: any) {
|
||||
const toastId = toast.loading("Iniciando generación de PDF...");
|
||||
|
||||
try {
|
||||
// 1. Trigger: Iniciar la tarea en Celery
|
||||
const { task_id } = await invoicesReportsApi.triggerPdfGeneration(
|
||||
invoice.id,
|
||||
companyStore.activeCompany.id
|
||||
);
|
||||
async function handleDownloadPdf(invoice: any) {
|
||||
if (!companyStore.activeCompany) {
|
||||
toast.error("No hay empresa seleccionada");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.loading("Procesando PDF en segundo plano...", { id: toastId });
|
||||
try {
|
||||
// 1. Trigger: Iniciar la tarea en Celery
|
||||
const { task_id } = await invoicesReportsApi.triggerPdfGeneration(
|
||||
invoice.id,
|
||||
companyStore.activeCompany.id
|
||||
);
|
||||
|
||||
// 2. Polling: Loop para verificar estado
|
||||
let intentos = 0;
|
||||
const maxIntentos = 30; // Timeout de seguridad (aprox 60 segs)
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
intentos++;
|
||||
try {
|
||||
const statusData = await invoicesReportsApi.getTaskStatus(task_id);
|
||||
// 2. Abrir diálogo de progreso
|
||||
currentTaskId = task_id;
|
||||
showProgressDialog = true;
|
||||
|
||||
if (statusData.state === 'SUCCESS') {
|
||||
clearInterval(interval);
|
||||
|
||||
const result = statusData.result; // Tu dict del backend
|
||||
|
||||
if (result.status === 'success') {
|
||||
// 3. Convertir Base64 a Blob y Descargar
|
||||
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; // Nombre que viene del worker
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("No se pudo iniciar la descarga");
|
||||
}
|
||||
}
|
||||
|
||||
toast.success("PDF Descargado", { id: toastId });
|
||||
} else {
|
||||
toast.error("Error al generar el archivo", { id: toastId });
|
||||
}
|
||||
}
|
||||
else if (statusData.state === 'FAILURE') {
|
||||
clearInterval(interval);
|
||||
toast.error("Falló la generación del PDF", { id: toastId });
|
||||
}
|
||||
else if (intentos >= maxIntentos) {
|
||||
clearInterval(interval);
|
||||
toast.error("Tiempo de espera agotado", { id: toastId });
|
||||
}
|
||||
// Si es PENDING o STARTED, el intervalo continúa...
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
clearInterval(interval); // Detener en caso de error de red
|
||||
toast.error("Error de conexión", { id: toastId });
|
||||
function onPdfComplete(result: any) {
|
||||
// Esta función se llama cuando el diálogo reporta SUCCESS
|
||||
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("PDF Descargado exitosamente");
|
||||
} else {
|
||||
toast.error("El worker reportó un error: " + (result.message || "Desconocido"));
|
||||
}
|
||||
}, 2000); // Consultar cada 2 segundos
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("No se pudo iniciar la descarga", { id: toastId });
|
||||
} catch (e) {
|
||||
console.error("Error al procesar descarga:", e);
|
||||
toast.error("Error al procesar el archivo descargado");
|
||||
} finally {
|
||||
// Cerrar diálogo después de un breve momento
|
||||
setTimeout(() => {
|
||||
showProgressDialog = false;
|
||||
currentTaskId = null;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateClick() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -443,8 +451,13 @@ async function handleDownloadPdf(invoice: any) {
|
||||
);
|
||||
});
|
||||
|
||||
function closeProgressDialog() {
|
||||
showProgressDialog = false;
|
||||
currentTaskId = null;
|
||||
}
|
||||
|
||||
// --- AQUÍ PASAMOS LA FUNCIÓN DE DESCARGA A LAS COLUMNAS ---
|
||||
const columns = createColumns(handleSuccess, handleDownloadPdf);
|
||||
const columns = createColumns(handleSuccess);
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
@@ -556,7 +569,38 @@ async function handleDownloadPdf(invoice: any) {
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
selectedId={selectedInvoiceId}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
|
||||
<PdfProgressDialog
|
||||
bind:open={showProgressDialog}
|
||||
taskId={currentTaskId}
|
||||
onComplete={onPdfComplete}
|
||||
onClose={closeProgressDialog}
|
||||
/>
|
||||
|
||||
<!-- Footer fijo con botones de acción -->
|
||||
<div class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] ml-[calc(var(--sidebar-width))]">
|
||||
<div class="px-4 py-4 max-w-[1400px] mx-auto">
|
||||
<!-- Botones de acción -->
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm" disabled={!selectedInvoice}>
|
||||
<RefreshCw class="h-4 w-4 mr-2" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={!selectedInvoice}>
|
||||
<RotateCcw class="h-4 w-4 mr-2" />
|
||||
Desactualizar
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onclick={() => selectedInvoice && handleDownloadPdf(selectedInvoice)} disabled={!selectedInvoice}>
|
||||
<FileDown class="h-4 w-4 mr-2" />
|
||||
Descargar PDF
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user