WIP: Guardando trabajo antes de actualizar con development

This commit is contained in:
2026-01-09 08:26:44 -06:00
parent c95c7f7c55
commit ff2df90dbe
11 changed files with 1315 additions and 449 deletions

View File

@@ -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';

View File

@@ -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>