WIP: Guardando trabajo antes de actualizar con development
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/ports/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/ports/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/data-table.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
|
||||
@@ -1,496 +1,543 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { invoicesApi, type Invoice, type OperationType } from '$lib/api/dashboard/a76/invoices';
|
||||
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 { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
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 { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { invoicesApi, type Invoice, type OperationType } from '$lib/api/dashboard/a76/invoices';
|
||||
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 { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
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';
|
||||
|
||||
// IMPORTANTE: Asegúrate de tener instalada svelte-sonner para las notificaciones
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Estado para filtros
|
||||
// Nota: Los query parameters invoice_type y operation_type se pueden usar para filtrar
|
||||
// Ejemplo: /dashboard/invoices?invoice_type=TEM&operation_type=imp
|
||||
let filters = $state({
|
||||
operation_type: (data.filters?.operation_type || '') as '' | OperationType,
|
||||
invoice_type: data.filters?.invoice_type || '',
|
||||
invoice_number: data.filters?.invoice_number || '',
|
||||
project_number: data.filters?.project_number || '',
|
||||
year: data.filters?.year || ''
|
||||
});
|
||||
// Estado para filtros
|
||||
// Nota: Los query parameters invoice_type y operation_type se pueden usar para filtrar
|
||||
// Ejemplo: /dashboard/invoices?invoice_type=TEM&operation_type=imp
|
||||
let filters = $state({
|
||||
operation_type: (data.filters?.operation_type || '') as '' | OperationType,
|
||||
invoice_type: data.filters?.invoice_type || '',
|
||||
invoice_number: data.filters?.invoice_number || '',
|
||||
project_number: data.filters?.project_number || '',
|
||||
year: data.filters?.year || ''
|
||||
});
|
||||
|
||||
// Efecto reactivo para actualizar filtros cuando cambian los query parameters en la URL
|
||||
$effect(() => {
|
||||
if (browser) {
|
||||
const searchParams = $page.url.searchParams;
|
||||
const urlOperationType = searchParams.get('operation_type');
|
||||
const urlInvoiceType = searchParams.get('invoice_type');
|
||||
const urlInvoiceNumber = searchParams.get('invoice_number');
|
||||
const urlProjectNumber = searchParams.get('project_number');
|
||||
const urlYear = searchParams.get('year');
|
||||
// Efecto reactivo para actualizar filtros cuando cambian los query parameters en la URL
|
||||
$effect(() => {
|
||||
if (browser) {
|
||||
const searchParams = $page.url.searchParams;
|
||||
const urlOperationType = searchParams.get('operation_type');
|
||||
const urlInvoiceType = searchParams.get('invoice_type');
|
||||
const urlInvoiceNumber = searchParams.get('invoice_number');
|
||||
const urlProjectNumber = searchParams.get('project_number');
|
||||
const urlYear = searchParams.get('year');
|
||||
|
||||
// Actualizar filtros si hay cambios en la URL
|
||||
filters.operation_type = (urlOperationType || '') as '' | OperationType;
|
||||
filters.invoice_type = urlInvoiceType || '';
|
||||
filters.invoice_number = urlInvoiceNumber || '';
|
||||
filters.project_number = urlProjectNumber || '';
|
||||
filters.year = urlYear || '';
|
||||
}
|
||||
});
|
||||
// Actualizar filtros si hay cambios en la URL
|
||||
filters.operation_type = (urlOperationType || '') as '' | OperationType;
|
||||
filters.invoice_type = urlInvoiceType || '';
|
||||
filters.invoice_number = urlInvoiceNumber || '';
|
||||
filters.project_number = urlProjectNumber || '';
|
||||
filters.year = urlYear || '';
|
||||
}
|
||||
});
|
||||
|
||||
// Efecto para limpiar invoice_type si no es válido para el operation_type seleccionado
|
||||
$effect(() => {
|
||||
if (filters.invoice_type && filters.operation_type) {
|
||||
const selectedOption = allInvoiceTypeOptions().find(opt => opt.value === filters.invoice_type);
|
||||
if (selectedOption && selectedOption.operation !== 'both' && selectedOption.operation !== filters.operation_type) {
|
||||
// El tipo de factura seleccionado no es válido para esta operación
|
||||
filters.invoice_type = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
// Efecto para limpiar invoice_type si no es válido para el operation_type seleccionado
|
||||
$effect(() => {
|
||||
if (filters.invoice_type && filters.operation_type) {
|
||||
const selectedOption = allInvoiceTypeOptions().find(opt => opt.value === filters.invoice_type);
|
||||
if (selectedOption && selectedOption.operation !== 'both' && selectedOption.operation !== filters.operation_type) {
|
||||
// El tipo de factura seleccionado no es válido para esta operación
|
||||
filters.invoice_type = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Efecto para actualizar la URL cuando cambien los filtros
|
||||
$effect(() => {
|
||||
if (browser) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters.operation_type) params.set('operation_type', filters.operation_type);
|
||||
if (filters.invoice_type) params.set('invoice_type', filters.invoice_type);
|
||||
if (filters.invoice_number) params.set('invoice_number', filters.invoice_number);
|
||||
if (filters.project_number) params.set('project_number', filters.project_number);
|
||||
if (filters.year) params.set('year', filters.year);
|
||||
|
||||
const queryString = params.toString();
|
||||
const newUrl = queryString ? `?${queryString}` : window.location.pathname;
|
||||
|
||||
// Solo actualizar si la URL es diferente (evitar loops infinitos)
|
||||
if (window.location.search !== (queryString ? `?${queryString}` : '')) {
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Efecto para actualizar la URL cuando cambien los filtros
|
||||
$effect(() => {
|
||||
if (browser) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters.operation_type) params.set('operation_type', filters.operation_type);
|
||||
if (filters.invoice_type) params.set('invoice_type', filters.invoice_type);
|
||||
if (filters.invoice_number) params.set('invoice_number', filters.invoice_number);
|
||||
if (filters.project_number) params.set('project_number', filters.project_number);
|
||||
if (filters.year) params.set('year', filters.year);
|
||||
|
||||
const queryString = params.toString();
|
||||
const newUrl = queryString ? `?${queryString}` : window.location.pathname;
|
||||
|
||||
// Solo actualizar si la URL es diferente (evitar loops infinitos)
|
||||
if (window.location.search !== (queryString ? `?${queryString}` : '')) {
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Efecto para aplicar filtros automáticamente cuando cambian
|
||||
let filterTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
$effect(() => {
|
||||
// Observar cambios en los filtros
|
||||
const _ = {
|
||||
operation_type: filters.operation_type,
|
||||
invoice_type: filters.invoice_type,
|
||||
invoice_number: filters.invoice_number,
|
||||
project_number: filters.project_number,
|
||||
year: filters.year
|
||||
};
|
||||
// Efecto para aplicar filtros automáticamente cuando cambian
|
||||
let filterTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
$effect(() => {
|
||||
// Observar cambios en los filtros
|
||||
const _ = {
|
||||
operation_type: filters.operation_type,
|
||||
invoice_type: filters.invoice_type,
|
||||
invoice_number: filters.invoice_number,
|
||||
project_number: filters.project_number,
|
||||
year: filters.year
|
||||
};
|
||||
|
||||
// Debounce para evitar múltiples llamadas rápidas
|
||||
if (filterTimeout) clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(() => {
|
||||
applyFilters();
|
||||
}, 300); // Esperar 300ms después del último cambio
|
||||
});
|
||||
// Debounce para evitar múltiples llamadas rápidas
|
||||
if (filterTimeout) clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(() => {
|
||||
applyFilters();
|
||||
}, 300); // Esperar 300ms después del último cambio
|
||||
});
|
||||
|
||||
// Sincronizar token de cookies a localStorage al montar el componente
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
// Función para obtener el valor de una cookie
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
// Sincronizar token de cookies a localStorage al montar el componente
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
// Función para obtener el valor de una cookie
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
|
||||
// Verificar si hay token en las cookies
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
// Verificar si hay token en las cookies
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
|
||||
if (cookieToken && cookieToken !== localToken) {
|
||||
localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
if (cookieToken && cookieToken !== localToken) {
|
||||
localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
|
||||
// También sincronizar refresh_token si existe
|
||||
const cookieRefreshToken = getCookie('refresh_token');
|
||||
const localRefreshToken = localStorage.getItem('refresh_token');
|
||||
// También sincronizar refresh_token si existe
|
||||
const cookieRefreshToken = getCookie('refresh_token');
|
||||
const localRefreshToken = localStorage.getItem('refresh_token');
|
||||
|
||||
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
|
||||
localStorage.setItem('refresh_token', cookieRefreshToken);
|
||||
}
|
||||
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
|
||||
localStorage.setItem('refresh_token', cookieRefreshToken);
|
||||
}
|
||||
|
||||
// Escuchar cambios de compañía
|
||||
const handleCompanyChange = (event: CustomEvent) => {
|
||||
// Recargar los datos sin recargar la página completa
|
||||
reloadData();
|
||||
};
|
||||
// Escuchar cambios de compañía
|
||||
const handleCompanyChange = (event: CustomEvent) => {
|
||||
// Recargar los datos sin recargar la página completa
|
||||
reloadData();
|
||||
};
|
||||
|
||||
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
};
|
||||
}
|
||||
});
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<Invoice[]>(data.items || []);
|
||||
let currentPage = $state(data.page);
|
||||
let pageSize = $state(50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<Invoice[]>(data.items || []);
|
||||
let currentPage = $state(data.page);
|
||||
let pageSize = $state(50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
const filterParams = {
|
||||
operation_type: filters.operation_type || undefined,
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, currentPage + 1, pageSize, filterParams);
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
const filterParams = {
|
||||
operation_type: filters.operation_type || undefined,
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, currentPage + 1, pageSize, filterParams);
|
||||
|
||||
if (response.error) {
|
||||
console.error('📊 [Invoices] Error en loadMore:', response.error, 'Status:', response.status);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.error) {
|
||||
console.error('📊 [Invoices] Error en loadMore:', response.error, 'Status:', response.status);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data?.items) {
|
||||
// Agregar los nuevos items al array existente
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [Invoices] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
if (response.data?.items) {
|
||||
// Agregar los nuevos items al array existente
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [Invoices] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyFilters() {
|
||||
// Reset y recargar con filtros
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
// Construir query parameters para el endpoint
|
||||
// Los filtros se mapean a los parámetros del API
|
||||
const filterParams = {
|
||||
operation_type: filters.operation_type || undefined,
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, 1, pageSize, filterParams);
|
||||
async function applyFilters() {
|
||||
// Reset y recargar con filtros
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
// Construir query parameters para el endpoint
|
||||
// Los filtros se mapean a los parámetros del API
|
||||
const filterParams = {
|
||||
operation_type: filters.operation_type || undefined,
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, 1, pageSize, filterParams);
|
||||
|
||||
if (response.error) {
|
||||
console.error('📊 [Invoices] Error aplicando filtros:', response.error);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.error) {
|
||||
console.error('📊 [Invoices] Error aplicando filtros:', response.error);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('📊 [Invoices] Error applying filters:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('📊 [Invoices] Error applying filters:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
filters = {
|
||||
operation_type: '',
|
||||
invoice_type: '',
|
||||
invoice_number: '',
|
||||
project_number: '',
|
||||
year: ''
|
||||
};
|
||||
applyFilters();
|
||||
}
|
||||
function clearFilters() {
|
||||
filters = {
|
||||
operation_type: '',
|
||||
invoice_type: '',
|
||||
invoice_number: '',
|
||||
project_number: '',
|
||||
year: ''
|
||||
};
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
// Reset y recargar desde el principio usando la API
|
||||
if (!companyStore.activeCompany) return;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
async function reloadData() {
|
||||
// Reset y recargar desde el principio usando la API
|
||||
if (!companyStore.activeCompany) return;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany.id;
|
||||
|
||||
const filterParams = {
|
||||
operation_type: filters.operation_type || undefined,
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
};
|
||||
try {
|
||||
const companyId = companyStore.activeCompany.id;
|
||||
|
||||
const filterParams = {
|
||||
operation_type: filters.operation_type || undefined,
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, 1, pageSize, filterParams);
|
||||
const response = await invoicesApi.list(companyId, 1, pageSize, filterParams);
|
||||
|
||||
if (response.error) {
|
||||
console.error('📊 [Invoices] Error en reloadData:', response.error);
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.error) {
|
||||
console.error('📊 [Invoices] Error en reloadData:', response.error);
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data?.items) {
|
||||
// Reemplazar todos los items con los nuevos datos
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error recargando datos';
|
||||
console.error('📊 [Invoices] Error reloading:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
if (response.data?.items) {
|
||||
// Reemplazar todos los items con los nuevos datos
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error recargando datos';
|
||||
console.error('📊 [Invoices] Error reloading:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateClick() {
|
||||
// Leer los filtros actuales desde la URL (que ya se actualizó con el $effect)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
// Mapear operation_type de 'imp'/'exp' a números 1/2
|
||||
const operationType = params.get('operation_type');
|
||||
if (operationType) {
|
||||
const operationTypeNumber = operationType === 'exp' ? 1 : 2;
|
||||
params.set('operation_type', operationTypeNumber.toString());
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = queryString
|
||||
? `/dashboard/invoices/edit/new?${queryString}`
|
||||
: '/dashboard/invoices/edit/new';
|
||||
|
||||
window.location.href = url;
|
||||
}
|
||||
// --- NUEVA FUNCIÓN: DESCARGAR PDF ---
|
||||
async function handleDownloadPdf(invoice: any) {
|
||||
const toastId = toast.loading("Generando PDF...");
|
||||
|
||||
try {
|
||||
// Obtenemos el token del localStorage
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) throw new Error('No hay sesión activa');
|
||||
|
||||
function handleSuccess() {
|
||||
// Recargar datos después de crear/editar/eliminar
|
||||
reloadData();
|
||||
}
|
||||
// Determinar tipo: Si es importación (imp) -> mex, Si es exportación (exp) -> usa (o lo que definas)
|
||||
// Por ahora hardcodeamos 'mex' como pediste
|
||||
const tipo = 'mex';
|
||||
|
||||
const endpoint = `/api/v1/a76/reports/importacion/facturas/${invoice.id}/download?tipo=${tipo}&formato=pdf`;
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
// Opciones de tipo de operación para el filtro
|
||||
const operationTypeOptions = [
|
||||
{ value: "", label: "Todas" },
|
||||
{ value: 'imp', label: 'Importación' },
|
||||
{ value: 'exp', label: 'Exportación' }
|
||||
];
|
||||
if (!response.ok) {
|
||||
const errData = await response.json().catch(() => ({}));
|
||||
throw new Error(errData.detail || 'Error al generar el reporte');
|
||||
}
|
||||
|
||||
// Todas las opciones de tipo de factura con su operación correspondiente
|
||||
// Ahora se cargan desde el servidor en lugar de estar hardcodeadas
|
||||
const allInvoiceTypeOptions = $derived(() => {
|
||||
const options = [{ value: "", label: "Todas", operation: "both" }];
|
||||
|
||||
// Agregar los tipos de factura del servidor
|
||||
if (data.invoiceTypes) {
|
||||
data.invoiceTypes.forEach((type: any) => {
|
||||
options.push({
|
||||
value: type.key,
|
||||
label: type.description,
|
||||
operation: type.operation
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
// Convertir respuesta a Blob
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Crear link fantasma y descargar
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
// Intentar usar nombre del header o fallback
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let fileName = `Factura_${invoice.invoice_number || invoice.id}.pdf`;
|
||||
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename="?([^"]+)"?/);
|
||||
if (match && match[1]) fileName = match[1];
|
||||
}
|
||||
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Limpieza
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
toast.success("Factura descargada", { id: toastId });
|
||||
|
||||
// Opciones de tipo de factura filtradas según el tipo de operación seleccionado
|
||||
const invoiceTypeOptions = $derived(() => {
|
||||
const allOptions = allInvoiceTypeOptions();
|
||||
|
||||
if (!filters.operation_type) {
|
||||
return allOptions;
|
||||
}
|
||||
|
||||
return allOptions.filter(option =>
|
||||
option.operation === 'both' ||
|
||||
option.operation === filters.operation_type
|
||||
);
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.message || "No se pudo descargar la factura", { id: toastId });
|
||||
}
|
||||
}
|
||||
|
||||
// Crear columnas con el callback onSuccess
|
||||
const columns = createColumns(handleSuccess);
|
||||
function handleCreateClick() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const operationType = params.get('operation_type');
|
||||
if (operationType) {
|
||||
const operationTypeNumber = operationType === 'exp' ? 1 : 2;
|
||||
params.set('operation_type', operationTypeNumber.toString());
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = queryString
|
||||
? `/dashboard/invoices/edit/new?${queryString}`
|
||||
: '/dashboard/invoices/edit/new';
|
||||
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Opciones de tipo de operación para el filtro
|
||||
const operationTypeOptions = [
|
||||
{ value: "", label: "Todas" },
|
||||
{ value: 'imp', label: 'Importación' },
|
||||
{ value: 'exp', label: 'Exportación' }
|
||||
];
|
||||
|
||||
// Todas las opciones de tipo de factura con su operación correspondiente
|
||||
const allInvoiceTypeOptions = $derived(() => {
|
||||
const options = [{ value: "", label: "Todas", operation: "both" }];
|
||||
if (data.invoiceTypes) {
|
||||
data.invoiceTypes.forEach((type: any) => {
|
||||
options.push({
|
||||
value: type.key,
|
||||
label: type.description,
|
||||
operation: type.operation
|
||||
});
|
||||
});
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
const invoiceTypeOptions = $derived(() => {
|
||||
const allOptions = allInvoiceTypeOptions();
|
||||
if (!filters.operation_type) {
|
||||
return allOptions;
|
||||
}
|
||||
return allOptions.filter(option =>
|
||||
option.operation === 'both' ||
|
||||
option.operation === filters.operation_type
|
||||
);
|
||||
});
|
||||
|
||||
// --- AQUÍ PASAMOS LA FUNCIÓN DE DESCARGA A LAS COLUMNAS ---
|
||||
const columns = createColumns(handleSuccess, handleDownloadPdf);
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona las facturas del sistema
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={handleCreateClick}>
|
||||
<Plus class="mr-2" size={16} />
|
||||
Nueva Factura
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona las facturas del sistema
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={handleCreateClick}>
|
||||
<Plus class="mr-2" size={16} />
|
||||
Nueva Factura
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Filtros -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Filtros</Card.Title>
|
||||
<Card.Description>Filtra las facturas por diferentes criterios (los filtros se aplican automáticamente)</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-operation-type">Tipo de Operación</Label>
|
||||
<select
|
||||
id="filter-operation-type"
|
||||
bind:value={filters.operation_type}
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
{#each operationTypeOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Filtros</Card.Title>
|
||||
<Card.Description>Filtra las facturas por diferentes criterios (los filtros se aplican automáticamente)</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-operation-type">Tipo de Operación</Label>
|
||||
<select
|
||||
id="filter-operation-type"
|
||||
bind:value={filters.operation_type}
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
{#each operationTypeOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-invoice-type">Tipo de Factura</Label>
|
||||
<select
|
||||
id="filter-invoice-type"
|
||||
bind:value={filters.invoice_type}
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
{#each invoiceTypeOptions() as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-invoice-type">Tipo de Factura</Label>
|
||||
<select
|
||||
id="filter-invoice-type"
|
||||
bind:value={filters.invoice_type}
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
{#each invoiceTypeOptions() as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-invoice-number">Número de Factura</Label>
|
||||
<Input
|
||||
id="filter-invoice-number"
|
||||
bind:value={filters.invoice_number}
|
||||
placeholder="Ej: INV-2024-001"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-invoice-number">Número de Factura</Label>
|
||||
<Input
|
||||
id="filter-invoice-number"
|
||||
bind:value={filters.invoice_number}
|
||||
placeholder="Ej: INV-2024-001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-project-number">Número de Proyecto</Label>
|
||||
<Input
|
||||
id="filter-project-number"
|
||||
bind:value={filters.project_number}
|
||||
placeholder="Ej: PROJ-001"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-project-number">Número de Proyecto</Label>
|
||||
<Input
|
||||
id="filter-project-number"
|
||||
bind:value={filters.project_number}
|
||||
placeholder="Ej: PROJ-001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-year">Año</Label>
|
||||
<Input
|
||||
id="filter-year"
|
||||
bind:value={filters.year}
|
||||
placeholder="Ej: 2024"
|
||||
maxlength={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-year">Año</Label>
|
||||
<Input
|
||||
id="filter-year"
|
||||
bind:value={filters.year}
|
||||
placeholder="Ej: 2024"
|
||||
maxlength={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<Card.Root class="border-destructive">
|
||||
<Card.Header>
|
||||
<Card.Title class="text-destructive">Error</Card.Title>
|
||||
<Card.Description>{error}</Card.Description>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
{#if error}
|
||||
<Card.Root class="border-destructive">
|
||||
<Card.Header>
|
||||
<Card.Title class="text-destructive">Error</Card.Title>
|
||||
<Card.Description>{error}</Card.Description>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
|
||||
<!-- Data Table -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Listado de Facturas</Card.Title>
|
||||
<Card.Description>
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</Card.Description>
|
||||
</div>
|
||||
<Button variant="outline" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2" size={16} />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<!-- TanStack DataTable con Infinite Scroll -->
|
||||
<DataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Listado de Facturas</Card.Title>
|
||||
<Card.Description>
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</Card.Description>
|
||||
</div>
|
||||
<Button variant="outline" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2" size={16} />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<DataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
Reference in New Issue
Block a user