Merge branch 'feature/table-invoice' into feature/API-COBE

This commit is contained in:
2026-04-13 13:51:01 -06:00
131 changed files with 6443 additions and 4454 deletions

View File

@@ -3,9 +3,10 @@ Service layer for Pedimentos CRUD operations
"""
import logging
import re
from typing import Any, Dict, List, Optional
from sqlalchemy import desc
from sqlalchemy import desc, func
from sqlalchemy.orm import Session, joinedload
from sqlalchemy.orm import selectinload
from sqlalchemy.exc import IntegrityError
@@ -95,10 +96,21 @@ class PedimentosService:
if filters.get("status"):
query = query.filter(Pedimentos.status == filters["status"])
if filters.get("client_id"):
query = query.filter(
Pedimentos.client_id == filters["client_id"])
query = query.filter(Pedimentos.client_id == filters["client_id"])
if filters.get("year"):
query = query.filter(Pedimentos.year == filters["year"])
if filters.get("pedimento"):
raw_value = str(filters["pedimento"])
# Normalizar: dejar solo dígitos (ignorar guiones, espacios, etc.)
normalized = re.sub(r"\D", "", raw_value)
if normalized:
ped_key_expr = func.concat(
func.coalesce(Pedimentos.year, ""),
func.coalesce(func.substr(Pedimentos.customs_office, 1, 2), ""),
func.coalesce(Pedimentos.license, ""),
func.coalesce(Pedimentos.pedimento_number, ""),
)
query = query.filter(ped_key_expr.ilike(f"%{normalized}%"))
total = query.count()

View File

@@ -15,11 +15,22 @@ router = APIRouter(prefix="/incoterms")
async def list_incoterms(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
code: str = Query(None, description="Filtrar por clave"),
description: str = Query(None, description="Filtrar por descripción"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
skip = (page - 1) * page_size
query = db.query(Incoterm)
if code:
query = query.filter(Incoterm.code.ilike(f"%{code}%"))
if description:
query = query.filter(
(Incoterm.description_es.ilike(f"%{description}%")) |
(Incoterm.description_en.ilike(f"%{description}%"))
)
items = query.offset(skip).limit(page_size).all()
total = query.count()
return {

View File

@@ -121,3 +121,45 @@
@apply bg-background text-foreground overflow-x-hidden;
}
}
@layer components {
.catalog-table-shell {
@apply rounded-md border border-border/80 bg-card shadow-sm;
}
.catalog-table-scroll {
@apply relative w-full flex-1 overflow-auto bg-card;
}
.catalog-table-header {
@apply sticky top-0 z-20 border-b border-border/80 bg-card/95 shadow-sm backdrop-blur-md;
}
.catalog-table-head-cell {
@apply whitespace-nowrap text-sm font-semibold text-foreground/90;
}
.catalog-table-row {
@apply transition-colors hover:bg-accent/35;
}
.catalog-table-row-selected {
@apply bg-accent/65 text-accent-foreground hover:bg-accent/65;
}
.catalog-table-sticky-left {
@apply sticky left-0 border-r border-border/70 bg-card shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)];
}
.catalog-table-sticky-right {
@apply sticky right-0 border-l border-border/70 bg-card shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.08)] dark:shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.25)];
}
.catalog-table-sticky-row-hover {
@apply bg-card group-hover/inv-list:bg-accent/35;
}
.catalog-table-sticky-row-selected {
@apply bg-accent/65 text-accent-foreground;
}
}

View File

@@ -40,11 +40,14 @@ import type { ApiResponse } from '$lib/api';
export async function getMultiCurrencyTypes(
companyId: number,
page?: number,
pageSize?: number
pageSize?: number,
filters?: { currency_type_code?: string; country_key?: string }
): Promise<ApiResponse<MultiCurrencyTypeListResponse>> {
const params = new URLSearchParams({ company_id: companyId.toString() });
if (page) params.append('page', page.toString());
if (pageSize) params.append('page_size', pageSize.toString());
if (filters?.currency_type_code) params.append('currency_type_code', filters.currency_type_code);
if (filters?.country_key) params.append('country_key', filters.country_key);
return api.get<MultiCurrencyTypeListResponse>(`/v1/a76/multi-currency-types/?${params.toString()}`);
}

View File

@@ -273,6 +273,7 @@ export interface UpdatePedimentoData {
export interface PedimentoFilters {
status?: string;
client_id?: number;
pedimento?: string;
year?: string;
sort_by?: string;
sort_order?: 'asc' | 'desc' | string;

View File

@@ -35,13 +35,18 @@ export interface UpdateCodePedimentoRegimenData {
*/
export const codePedimentoRegimensApi = {
/**
* Lista todos los code pedimento regimens con paginación
* Lista todos los code pedimento regimens con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<CodePedimentoRegimenListResponse>(
// CORRECTO: Slash antes del signo '?'
`/v1/public/reference_data/code-pedimento-regimens/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/code-pedimento-regimens/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
return api.get<CodePedimentoRegimenListResponse>(url);
},
/**
* Obtiene un por ID

View File

@@ -31,15 +31,18 @@ export interface UpdateContainerData {
*/
export const containersApi = {
/**
* Lista todos los containers con paginación
* Lista todos los containers con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<ContainerListResponse>(
// CORRECTO: Slash antes del ?
`/v1/public/reference_data/containers/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/containers/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
return api.get<ContainerListResponse>(url);
},
/**
* Obtiene un container por ID

View File

@@ -34,15 +34,18 @@ export interface UpdateCurrencyTypeData {
*/
export const currencyTypesApi = {
/**
* Lista todos los tipos de moneda con paginación
* Lista todos los tipos de moneda con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<CurrencyTypeListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/reference_data/currency-types/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/currency-types/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
return api.get<CurrencyTypeListResponse>(url);
},
/**
* Obtiene un tipo de moneda por código

View File

@@ -31,14 +31,18 @@ export interface UpdateCustomsSectionData {
*/
export const customsSectionsApi = {
/**
* Lista todas las secciones aduaneras con paginación
* Lista todas las secciones aduaneras con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<CustomsSectionListResponse>(
`/v1/public/reference_data/customs-sections/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/customs-sections/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
return api.get<CustomsSectionListResponse>(url);
},
/**
* Obtiene una sección aduanera por código

View File

@@ -34,15 +34,18 @@ export interface UpdateCustomsWarehouseData {
*/
export const customsWarehousesApi = {
/**
* Lista todos los recintos fiscalizados con paginación
* Lista todos los recintos fiscalizados con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<CustomsWarehouseListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/reference_data/customs-warehouses/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/customs-warehouses/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
return api.get<CustomsWarehouseListResponse>(url);
},
/**
* Obtiene un recinto fiscalizado por clave compuesta (key + customs)

View File

@@ -34,15 +34,18 @@ export interface UpdateIncotermData {
*/
export const incotermsApi = {
/**
* Lista todos los incoterms con paginación
* Lista todos los incoterms con paginación y filtros
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param code - Filtrar por clave (opcional)
* @param description - Filtrar por descripción (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<IncotermListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, code?: string, description?: string) => {
let url = `/v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}`;
if (code) url += `&code=${encodeURIComponent(code)}`;
if (description) url += `&description=${encodeURIComponent(description)}`;
return api.get<IncotermListResponse>(url);
},
/**
* Obtiene un incoterm por código

View File

@@ -38,12 +38,13 @@ export interface UpdateInvoiceTypeData {
*/
export const invoiceTypesApi = {
/**
* Lista todos los tipos de factura con paginación
* Lista todos los tipos de factura con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param operation - Filtrar por tipo de operación (imp, exp)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50, operation?: string) => {
list: (page = 1, pageSize = 50, operation?: string, search?: string) => {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString()
@@ -51,8 +52,10 @@ export const invoiceTypesApi = {
if (operation) {
params.append('operation', operation);
}
if (search) {
params.append('search', search);
}
return api.get<InvoiceTypeListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/reference_data/invoice-types/?${params.toString()}`
);
},

View File

@@ -34,12 +34,13 @@
*/
export const materialTypesApi = {
/**
* Lista todos los tipos de material con paginación
* Lista todos los tipos de material con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param type - Filtrar por tipo (ACTIVO FIJO, MATERIALES, PRODUCTOS)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50, type?: string) => {
list: (page = 1, pageSize = 50, type?: string, search?: string) => {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString()
@@ -47,8 +48,10 @@ export const materialTypesApi = {
if (type) {
params.append('type', type);
}
if (search) {
params.append('search', search);
}
return api.get<MaterialTypeListResponse>(
// CORRECTO: Ya tiene el '/' antes del '?'
`/v1/public/reference_data/material-types/?${params.toString()}`
);
},

View File

@@ -31,15 +31,18 @@ export interface UpdatePaymentMethodData {
*/
export const paymentMethodsApi = {
/**
* Lista todos los métodos de pago con paginación
* Lista todos los métodos de pago con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<PaymentMethodListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/reference_data/payment-methods/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/payment-methods/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
return api.get<PaymentMethodListResponse>(url);
},
/**
* Obtiene un método de pago por key

View File

@@ -31,15 +31,18 @@ export interface UpdatePedimentoCodeData {
*/
export const pedimentoCodesApi = {
/**
* Lista todas las claves de pedimento con paginación
* Lista todas las claves de pedimento con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<PedimentoCodeListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/reference_data/pedimento-codes/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/pedimento-codes/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
return api.get<PedimentoCodeListResponse>(url);
},
/**
* Obtiene una clave de pedimento por code

View File

@@ -31,15 +31,18 @@ export interface UpdatePedimentoRegimenData {
*/
export const pedimentoRegimensApi = {
/**
* Lista todos los regímenes de pedimento con paginación
* Lista todos los regímenes de pedimento con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<PedimentoRegimenListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/reference_data/pedimento-regimens/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/pedimento-regimens/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
return api.get<PedimentoRegimenListResponse>(url);
},
/**
* Obtiene un régimen de pedimento por code

View File

@@ -37,15 +37,18 @@ export interface UpdateStateData {
*/
export const statesApi = {
/**
* Lista todos los estados con paginación
* Lista todos los estados con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<StateListResponse>(
// CORREGIDO: Añadido '/' antes de '?'
`/v1/public/reference_data/states/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/states/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
return api.get<StateListResponse>(url);
},
/**
* Obtiene un estado por m3_key

View File

@@ -31,14 +31,18 @@ export interface UpdateTransportModeData {
*/
export const transportModesApi = {
/**
* Lista todos los modos de transporte con paginación
* Lista todos los modos de transporte con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<TransportModeListResponse>(
`/v1/public/reference_data/transport-modes/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/transport-modes/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
return api.get<TransportModeListResponse>(url);
},
/**
* Obtiene un modo de transporte por key

View File

@@ -31,12 +31,18 @@ export interface UpdateTransportTypeData {
*/
export const transportTypesApi = {
/**
* Lista todos los tipos de transporte con paginación
* Lista todos los tipos de transporte con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<TransportTypeListResponse>(
`/v1/public/reference_data/transport-types/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/transport-types/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
return api.get<TransportTypeListResponse>(url);
},
/**
* Obtiene un tipo de transporte por transport_code

View File

@@ -31,15 +31,18 @@ export interface UpdateValuationMethodData {
*/
export const valuationMethodsApi = {
/**
* Lista todos los métodos de valoración con paginación
* Lista todos los métodos de valoración con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50) =>
api.get<ValuationMethodListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/reference_data/valuation-methods/?page=${page}&page_size=${pageSize}`
),
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/valuation-methods/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
return api.get<ValuationMethodListResponse>(url);
},
/**
* Obtiene un método de valoración por key

View File

@@ -60,7 +60,10 @@
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<div
class="rounded-md border min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto"
bind:this={scrollContainer}
>
<Table.Root>
<Table.Header class="bg-background">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -0,0 +1,152 @@
<script lang="ts" generics="TData, TValue">
import { onMount } from 'svelte';
import { type ColumnDef, getCoreRowModel } from '@tanstack/table-core';
import { createSvelteTable, FlexRender } from '$lib/components/ui/data-table/index.js';
import * as Table from '$lib/components/ui/table/index.js';
type InfiniteDataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
loading: boolean;
hasMore: boolean;
loadMore: () => void;
emptyMessage?: string;
};
let {
data,
columns,
loading,
hasMore,
loadMore,
emptyMessage = 'No hay resultados.'
}: InfiniteDataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel()
});
let scrollContainer = $state<HTMLDivElement>();
let loadingTrigger = $state<HTMLDivElement>();
onMount(() => {
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting && hasMore && !loading) {
loadMore();
}
},
{
root: scrollContainer,
threshold: 0.1
}
);
if (loadingTrigger) {
observer.observe(loadingTrigger);
}
return () => {
observer.disconnect();
};
});
</script>
<div class="flex h-full w-full flex-col overflow-hidden">
<div
class="catalog-table-scroll min-h-[320px] max-h-[calc(100svh-280px)]"
bind:this={scrollContainer}
>
<Table.Root>
<Table.Header class="catalog-table-header">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
{@const headerList = headerGroup.headers}
{@const lastHeaderColId = headerList[headerList.length - 1]?.column.id}
<Table.Row>
{#each headerList as header (header.id)}
{@const colId = header.column.id}
<Table.Head
class={[
'catalog-table-head-cell',
colId === lastHeaderColId &&
'catalog-table-sticky-right z-30'
]
.filter(Boolean)
.join(' ')}
>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#if table.getRowModel().rows.length}
{#each table.getRowModel().rows as row (row.id)}
{@const visibleCells = row.getVisibleCells()}
{@const lastCellColId = visibleCells[visibleCells.length - 1]?.column.id}
<Table.Row
data-state={row.getIsSelected() && 'selected'}
class="catalog-table-row"
>
{#each visibleCells as cell (cell.id)}
{@const colId = cell.column.id}
<Table.Cell
class={[
'whitespace-nowrap',
colId === lastCellColId &&
'catalog-table-sticky-right z-10'
]
.filter(Boolean)
.join(' ')}
>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center text-sm text-muted-foreground">
{emptyMessage}
</Table.Cell>
</Table.Row>
{/if}
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex h-full w-full items-center justify-center">
{#if loading}
<div class="flex items-center justify-center gap-3 rounded-full border bg-muted/30 px-6 py-2 shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-sm font-medium text-foreground">Cargando más registros...</span>
</div>
{:else}
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
</div>

View File

@@ -71,7 +71,10 @@
</script>
<div class="w-full">
<div class="max-h-[600px] overflow-y-auto rounded-md border" bind:this={scrollContainer}>
<div
class="rounded-md border min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto"
bind:this={scrollContainer}
>
<Table.Root>
<Table.Header class="sticky top-0 z-10 bg-background">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -67,14 +67,18 @@
});
</script>
<div class="w-full">
<div class="max-h-[600px] overflow-y-auto rounded-md border" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="w-full h-full">
<div
class="catalog-table-shell min-h-[360px] max-h-[calc(100svh-420px)] overflow-auto [&_[data-slot=table-container]]:w-full"
bind:this={scrollContainer}
>
<div class="min-w-max">
<Table.Root>
<Table.Header class="catalog-table-header">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<Table.Head class="catalog-table-head-cell">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
@@ -91,9 +95,7 @@
<Table.Row
data-state={row.getIsSelected() && 'selected'}
onclick={() => onRowClick?.(row.original)}
class="cursor-pointer transition-colors {row.getIsSelected()
? 'bg-gray-300 dark:bg-gray-600'
: 'hover:bg-gray-100 dark:hover:bg-gray-700'}"
class="cursor-pointer {row.getIsSelected() ? 'catalog-table-row-selected' : 'catalog-table-row'}"
>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
@@ -103,7 +105,7 @@
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<Table.Cell colspan={columns.length} class="h-24 text-center text-sm text-muted-foreground">
No hay resultados.
</Table.Cell>
</Table.Row>
@@ -128,6 +130,7 @@
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</Table.Root>
</div>
</div>
</div>

View File

@@ -38,67 +38,69 @@
}
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#if table.getRowModel().rows.length}
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
<div class="flex h-full w-full flex-col overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner">
<Table.Root>
<Table.Header class="sticky top-0 z-20 border-b bg-background/95 shadow-sm backdrop-blur-md">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head class="whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems} registros
</Table.Header>
<Table.Body>
{#if table.getRowModel().rows.length}
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell class="whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 border-t px-4 py-3">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems} registros
</div>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
>
Siguiente
</Button>
</div>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
>
Siguiente
</Button>
</div>

View File

@@ -40,67 +40,69 @@
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
<div class="flex h-full w-full flex-col overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner">
<Table.Root>
<Table.Header class="sticky top-0 z-20 border-b bg-background/95 shadow-sm backdrop-blur-md">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head class="whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell class="whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4 px-2">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems} registros
<div class="flex items-center justify-end space-x-2 border-t px-4 py-3">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems} registros
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(currentPage - 1)}
disabled={currentPage <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(currentPage + 1)}
disabled={currentPage >= pageCount}
>
Siguiente
</Button>
</div>
</div>
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(currentPage - 1)}
disabled={currentPage <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(currentPage + 1)}
disabled={currentPage >= pageCount}
>
Siguiente
</Button>
</div>
</div>

View File

@@ -1,9 +1,8 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import * as Table from '$lib/components/ui/table';
import * as Card from '$lib/components/ui/card';
import { onMount, untrack } from 'svelte';
import { untrack } from 'svelte';
import { toast } from 'svelte-sonner';
import { getSectors, type Sector } from '$lib/api/dashboard/general_catalogs/sectors';
import { Loader2, Search } from 'lucide-svelte';
@@ -103,34 +102,39 @@
});
</script>
<div class="space-y-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<h2 class="text-3xl font-bold tracking-tight">{title}</h2>
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
</div>
<Card.Root>
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
<Card.Header>
<div class="flex w-full items-center gap-4">
<div class="relative flex-1">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar por clave o descripción..."
class="pl-8"
bind:value={searchTerm}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Sectores</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar por clave o descripción..."
class="h-9 w-44 bg-card pl-8 lg:w-64"
bind:value={searchTerm}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
</div>
</div>
</div>
</Card.Header>
<Card.Content>
<div class="rounded-md border">
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-0 flex-1 overflow-auto">
<Table.Root>
<Table.Header>
<Table.Header class="catalog-table-header">
<Table.Row>
<Table.Head class="w-[100px]">Clave</Table.Head>
<Table.Head>Descripción</Table.Head>
<Table.Head class="text-right">Estatus</Table.Head>
<Table.Head class="catalog-table-head-cell w-[100px]">Clave</Table.Head>
<Table.Head class="catalog-table-head-cell">Descripción</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">Estatus</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
@@ -150,7 +154,7 @@
</Table.Row>
{:else}
{#each sectors as sector}
<Table.Row>
<Table.Row class="catalog-table-row">
<Table.Cell class="font-medium">{sector.key}</Table.Cell>
<Table.Cell>{sector.description}</Table.Cell>
<Table.Cell class="text-right">
@@ -174,15 +178,13 @@
{/if}
</Table.Body>
</Table.Root>
</div>
<div class="mt-4 flex flex-col items-center gap-2">
<div class="text-xs text-muted-foreground">
Mostrando {sectors.length} de {total} registros
</div>
<!-- Infinite Scroll Sentinel -->
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {sectors.length} de {total} registros</div>
<!-- Infinite Scroll Sentinel -->
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, untrack } from 'svelte';
import { untrack } from 'svelte';
import {
getCanadianFractions,
deleteCanadianFraction,
@@ -8,6 +8,7 @@
import * as Table from '$lib/components/ui/table';
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { Search, Loader2, Plus, Pencil, Trash2 } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import CanadianFractionDialog from './CanadianFractionDialog.svelte';
@@ -25,6 +26,7 @@
let searchTimeout: ReturnType<typeof setTimeout>;
let observer: IntersectionObserver;
let sentinel: HTMLDivElement;
let scrollContainer: HTMLDivElement;
// Infinite scroll state
let hasMore = $state(true);
@@ -138,7 +140,7 @@
loadFractions(false);
}
},
{ rootMargin: '100px' }
{ root: scrollContainer || null, rootMargin: '100px' }
);
if (sentinel) observer.observe(sentinel);
@@ -162,101 +164,99 @@
});
</script>
<div class="space-y-4">
<div class="flex flex-col items-end justify-between gap-4 md:flex-row">
<div class="flex max-w-2xl flex-1 items-end gap-4">
<div class="flex-1">
<label for="search-fraction" class="mb-2 block text-sm font-medium">Buscar</label>
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-fraction"
type="text"
placeholder="Buscar por fracción o descripción..."
class="pl-9"
bind:value={searchQuery}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden">
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Fracciones Canadienses</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-fraction"
type="text"
placeholder="Buscar por fracción o descripción..."
class="h-9 w-44 bg-card pl-9 lg:w-64"
bind:value={searchQuery}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
</div>
<Button class="h-9" onclick={handleCreate}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
</div>
</div>
</div>
<Button onclick={handleCreate}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
</div>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Fracción</Table.Head>
<Table.Head>Descripción</Table.Head>
<Table.Head>País</Table.Head>
<Table.Head>Unidad</Table.Head>
<Table.Head class="text-right">ADV</Table.Head>
<Table.Head class="w-[100px]">Acciones</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if fractions.length === 0 && !loading}
<Table.Row>
<Table.Cell colspan={6} class="h-24 text-center"
>No se encontraron resultados</Table.Cell
>
</Table.Row>
{:else}
{#each fractions as fraction}
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="catalog-table-header">
<Table.Row>
<Table.Cell class="font-medium">{fraction.fraction}</Table.Cell>
<Table.Cell>{fraction.description || '-'}</Table.Cell>
<Table.Cell>{fraction.country_code}</Table.Cell>
<Table.Cell>{fraction.unit_of_measure || '-'}</Table.Cell>
<Table.Cell class="text-right">{fraction.ad_valorem ?? '-'}</Table.Cell>
<Table.Cell>
<div class="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
onclick={() => handleEdit(fraction)}
>
<Pencil class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => handleDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
<Trash2 class="h-4 w-4" />
{/if}
</Button>
</div>
</Table.Cell>
<Table.Head class="catalog-table-head-cell">Fracción</Table.Head>
<Table.Head class="catalog-table-head-cell">Descripción</Table.Head>
<Table.Head class="catalog-table-head-cell">País</Table.Head>
<Table.Head class="catalog-table-head-cell">Unidad</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">ADV</Table.Head>
<Table.Head class="catalog-table-head-cell w-[100px]">Acciones</Table.Head>
</Table.Row>
{/each}
{/if}
{#if loading}
<Table.Row>
<Table.Cell colspan={6} class="h-24 text-center">
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
</Table.Header>
<Table.Body>
{#if fractions.length === 0 && !loading}
<Table.Row>
<Table.Cell colspan={6} class="h-24 text-center">No se encontraron resultados</Table.Cell>
</Table.Row>
{:else}
{#each fractions as fraction}
<Table.Row class="catalog-table-row">
<Table.Cell class="font-medium">{fraction.fraction}</Table.Cell>
<Table.Cell>{fraction.description || '-'}</Table.Cell>
<TableCell>{fraction.country_code}</TableCell>
<TableCell>{fraction.unit_of_measure || '-'}</TableCell>
<TableCell class="text-right">{fraction.ad_valorem ?? '-'}</TableCell>
<TableCell>
<div class="flex items-center gap-2">
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => handleEdit(fraction)}>
<Pencil class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => handleDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
<Trash2 class="h-4 w-4" />
{/if}
</Button>
</div>
</TableCell>
</Table.Row>
{/each}
{/if}
{#if loading}
<Table.Row>
<Table.Cell colspan={6} class="h-24 text-center">
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>
</div>
</Card.Content>
</Card.Root>
<!-- Infinite Scroll Sentinel -->
<div bind:this={sentinel} class="h-4 w-full"></div>
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalItems} registros</div>
<CanadianFractionDialog
bind:open={dialogOpen}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, untrack } from 'svelte';
import { untrack } from 'svelte';
import {
getHistoricalFractions,
deleteHistoricalFraction,
@@ -8,23 +8,27 @@
import * as Table from '$lib/components/ui/table';
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { Search, Loader2, Plus, Pencil, Trash2 } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import HistoricalFractionDialog from './HistoricalFractionDialog.svelte';
import { companyStore } from '$lib/stores/company.svelte';
import { companyStore } from '$lib/stores/company.svelte';
let fractions = $state<HistoricalFraction[]>([]);
let loading = $state(false);
let historicalFraction = $state('');
let page = $state(1);
let totalItems = $state(0);
let totalPages = $state(0);
let pageSize = 50;
let { title = 'Fracciones históricas' }: { title?: string } = $props();
let searchTimeout: ReturnType<typeof setTimeout>;
let observer: IntersectionObserver;
let sentinel: HTMLDivElement;
let fractions = $state<HistoricalFraction[]>([]);
let loading = $state(false);
let historicalFraction = $state('');
let page = $state(1);
let totalItems = $state(0);
let totalPages = $state(0);
let pageSize = 50;
let searchTimeout: ReturnType<typeof setTimeout>;
let observer: IntersectionObserver;
let sentinel: HTMLDivElement;
let scrollContainer: HTMLDivElement;
// Infinite scroll state
let hasMore = $state(true);
@@ -138,7 +142,10 @@
loadFractions(false);
}
},
{ rootMargin: '100px' }
{
root: scrollContainer || null,
rootMargin: '100px'
}
);
if (sentinel) observer.observe(sentinel);
@@ -163,117 +170,116 @@
});
</script>
<div class="space-y-4">
<div class="flex flex-col items-end justify-between gap-4 md:flex-row">
<div class="flex max-w-2xl flex-1 items-end gap-4">
<div class="flex-1">
<label for="search-fraction" class="mb-2 block text-sm font-medium"
>Fracción Histórica</label
>
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-fraction"
type="text"
placeholder="Buscar fracción..."
class="pl-9"
bind:value={historicalFraction}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
</div>
</div>
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<!-- Header -->
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
<p class="text-sm text-muted-foreground">
Gestiona las fracciones históricas de la tarifa.
</p>
</div>
<Button onclick={handleCreate}>
<Button class="h-9" onclick={handleCreate}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
</div>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Fracción</Table.Head>
<Table.Head>Tipo</Table.Head>
<Table.Head>UM</Table.Head>
<Table.Head>País</Table.Head>
<Table.Head>Fecha Pub.</Table.Head>
<Table.Head>Fecha Fin</Table.Head>
<Table.Head class="text-right">IGI</Table.Head>
<Table.Head class="text-right">IGE</Table.Head>
<Table.Head class="w-[100px]">Acciones</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if fractions.length === 0 && !loading}
<Table.Row>
<Table.Cell colspan={9} class="h-24 text-center"
>No se encontraron resultados</Table.Cell
>
</Table.Row>
{:else}
{#each fractions as fraction}
<Table.Row>
<Table.Cell class="font-medium">{fraction.historical_fraction}</Table.Cell>
<Table.Cell>{fraction.fraction_type || '-'}</Table.Cell>
<Table.Cell>{fraction.unit_of_measure_code || '-'}</Table.Cell>
<Table.Cell>{fraction.country || '-'}</Table.Cell>
<Table.Cell
>{fraction.publication_date
? new Date(fraction.publication_date).toLocaleDateString()
: '-'}</Table.Cell
>
<Table.Cell
>{fraction.end_date
? new Date(fraction.end_date).toLocaleDateString()
: '-'}</Table.Cell
>
<Table.Cell class="text-right">{fraction.import_tax_rate ?? '-'}</Table.Cell>
<Table.Cell class="text-right">{fraction.export_tax_rate ?? '-'}</Table.Cell>
<Table.Cell>
<div class="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
onclick={() => handleEdit(fraction)}
>
<Pencil class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => handleDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
<Trash2 class="h-4 w-4" />
{/if}
</Button>
</div>
</Table.Cell>
</Table.Row>
{/each}
{/if}
{#if loading}
<Table.Row>
<Table.Cell colspan={9} class="h-24 text-center">
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
<Card.Root class="flex min-h-0 flex-1 flex-col border bg-background">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Fracciones Históricas</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-fraction"
type="text"
placeholder="Buscar fracción..."
class="h-9 w-40 bg-card pl-9 lg:w-56"
bind:value={historicalFraction}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
</div>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="catalog-table-header">
<Table.Row>
<Table.Head class="catalog-table-head-cell">Fracción</Table.Head>
<Table.Head class="catalog-table-head-cell">Tipo</Table.Head>
<Table.Head class="catalog-table-head-cell">UM</Table.Head>
<Table.Head class="catalog-table-head-cell">País</Table.Head>
<Table.Head class="catalog-table-head-cell">Fecha Pub.</Table.Head>
<Table.Head class="catalog-table-head-cell">Fecha Fin</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">IGI</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">IGE</Table.Head>
<Table.Head class="catalog-table-head-cell w-[100px]">Acciones</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if fractions.length === 0 && !loading}
<Table.Row>
<Table.Cell colspan={9} class="h-24 text-center">No se encontraron resultados</Table.Cell>
</Table.Row>
{:else}
{#each fractions as fraction}
<Table.Row class="catalog-table-row">
<Table.Cell class="font-medium">{fraction.historical_fraction}</Table.Cell>
<Table.Cell>{fraction.fraction_type || '-'}</Table.Cell>
<Table.Cell>{fraction.unit_of_measure_code || '-'}</Table.Cell>
<Table.Cell>{fraction.country || '-'}</Table.Cell>
<Table.Cell>{fraction.publication_date ? new Date(fraction.publication_date).toLocaleDateString() : '-'}</Table.Cell>
<Table.Cell>{fraction.end_date ? new Date(fraction.end_date).toLocaleDateString() : '-'}</Table.Cell>
<Table.Cell class="text-right">{fraction.import_tax_rate ?? '-'}</Table.Cell>
<Table.Cell class="text-right">{fraction.export_tax_rate ?? '-'}</Table.Cell>
<Table.Cell>
<div class="flex items-center gap-2">
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => handleEdit(fraction)}>
<Pencil class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => handleDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
<Trash2 class="h-4 w-4" />
{/if}
</Button>
</div>
</Table.Cell>
</Table.Row>
{/each}
{/if}
{#if loading}
<Table.Row>
<Table.Cell colspan={9} class="h-24 text-center">
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>
</div>
</Card.Content>
</Card.Root>
<!-- Infinite Scroll Sentinel -->
<div bind:this={sentinel} class="h-4 w-full"></div>
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalItems} registros</div>
<HistoricalFractionDialog
bind:open={dialogOpen}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label';
import * as Card from '$lib/components/ui/card';
import {
Table,
TableBody,
@@ -17,7 +17,7 @@
type TariffFraction
} from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
import { companyStore } from '$lib/stores/company.svelte';
import { onMount, untrack } from 'svelte';
import { untrack } from 'svelte';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import TariffFractionFormDialog from './TariffFractionFormDialog.svelte';
import { toast } from 'svelte-sonner';
@@ -43,6 +43,7 @@
let searchTimeout: ReturnType<typeof setTimeout>;
let observer: IntersectionObserver;
let sentinel: HTMLDivElement;
let scrollContainer: HTMLDivElement;
// Infinite scroll state
let hasMore = $state(true);
@@ -121,7 +122,7 @@
handlePageChange(currentPage + 1);
}
},
{ rootMargin: '100px' }
{ root: scrollContainer || null, rootMargin: '100px' }
);
if (sentinel) observer.observe(sentinel);
@@ -180,108 +181,118 @@
});
</script>
<div class="space-y-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-bold tracking-tight">{title}</h2>
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
{#if !readOnly}
<Button onclick={openCreateDialog}>
<Button class="h-9" onclick={openCreateDialog}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
{/if}
</div>
<div class="flex items-center space-x-2">
<div class="relative max-w-sm flex-1">
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
<Input placeholder="Buscar..." class="pl-8" bind:value={search} oninput={handleSearchInput} />
</div>
</div>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Clave</TableHead>
<TableHead>Fracción</TableHead>
<TableHead>Descripción</TableHead>
{#if catalog === 'mex'}
<TableHead>NICO</TableHead>
<TableHead>U.M.T</TableHead>
{:else}
<TableHead>Unidad</TableHead>
{/if}
<TableHead>Adv. Impo</TableHead>
<TableHead>Adv. Expo</TableHead>
{#if !readOnly}
<TableHead class="w-[100px]">Acciones</TableHead>
{/if}
</TableRow>
</TableHeader>
<TableBody>
{#if fractions.length === 0 && !isLoading}
<TableRow>
<TableCell
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
class="h-24 text-center"
>
No se encontraron resultados
</TableCell>
</TableRow>
{:else}
{#each fractions as fraction}
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Fracciones</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
<Input placeholder="Buscar..." class="h-9 w-40 bg-card pl-8 lg:w-56" bind:value={search} oninput={handleSearchInput} />
</div>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-auto" bind:this={scrollContainer}>
<Table>
<TableHeader class="catalog-table-header">
<TableRow>
<TableCell class="font-mono">{fraction.um_code || fraction.code}</TableCell>
<TableCell class="font-medium">{fraction.fraction}</TableCell>
<TableCell class="max-w-md truncate" title={fraction.description}>
{fraction.description}
</TableCell>
<TableHead class="catalog-table-head-cell">Clave</TableHead>
<TableHead class="catalog-table-head-cell">Fracción</TableHead>
<TableHead class="catalog-table-head-cell">Descripción</TableHead>
{#if catalog === 'mex'}
<TableCell>{fraction.nico || '-'}</TableCell>
<TableCell>{fraction.umt || '-'}</TableCell>
<TableHead class="catalog-table-head-cell">NICO</TableHead>
<TableHead class="catalog-table-head-cell">U.M.T</TableHead>
{:else}
<TableCell>{fraction.umt || '-'}</TableCell>
<TableHead class="catalog-table-head-cell">Unidad</TableHead>
{/if}
<TableCell>{fraction.adv_impo || '-'}</TableCell>
<TableCell>{fraction.adv_expo || '-'}</TableCell>
<TableHead class="catalog-table-head-cell">Adv. Impo</TableHead>
<TableHead class="catalog-table-head-cell">Adv. Expo</TableHead>
{#if !readOnly}
<TableCell>
<div class="flex items-center gap-2">
<Button variant="ghost" size="icon" onclick={() => openEditDialog(fraction)}>
<Edit class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="text-destructive hover:text-destructive"
onclick={() => confirmDelete(fraction)}
>
<Trash2 class="h-4 w-4" />
</Button>
</div>
</TableCell>
<TableHead class="catalog-table-head-cell w-[100px]">Acciones</TableHead>
{/if}
</TableRow>
{/each}
{/if}
{#if isLoading}
<TableRow>
<TableCell
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
class="h-24 text-center"
>
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</TableCell>
</TableRow>
{/if}
</TableBody>
</Table>
</div>
</TableHeader>
<TableBody>
{#if fractions.length === 0 && !isLoading}
<TableRow>
<TableCell
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
class="h-24 text-center"
>
No se encontraron resultados
</TableCell>
</TableRow>
{:else}
{#each fractions as fraction}
<TableRow class="catalog-table-row">
<TableCell class="font-mono">{fraction.um_code || fraction.code}</TableCell>
<TableCell class="font-medium">{fraction.fraction}</TableCell>
<TableCell class="max-w-md truncate" title={fraction.description}>
{fraction.description}
</TableCell>
{#if catalog === 'mex'}
<TableCell>{fraction.nico || '-'}</TableCell>
<TableCell>{fraction.umt || '-'}</TableCell>
{:else}
<TableCell>{fraction.umt || '-'}</TableCell>
{/if}
<TableCell>{fraction.adv_impo || '-'}</TableCell>
<TableCell>{fraction.adv_expo || '-'}</TableCell>
{#if !readOnly}
<TableCell>
<div class="flex items-center gap-2">
<Button variant="ghost" size="icon" onclick={() => openEditDialog(fraction)}>
<Edit class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="text-destructive hover:text-destructive"
onclick={() => confirmDelete(fraction)}
>
<Trash2 class="h-4 w-4" />
</Button>
</div>
</TableCell>
{/if}
</TableRow>
{/each}
{/if}
{#if isLoading}
<TableRow>
<TableCell
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
class="h-24 text-center"
>
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</TableCell>
</TableRow>
{/if}
</TableBody>
</Table>
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>
</div>
</Card.Content>
</Card.Root>
<!-- Infinite Scroll Sentinel -->
<div bind:this={sentinel} class="h-4 w-full"></div>
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalFractions} registros</div>
</div>
<AlertDialog.Root bind:open={showDeleteConfirm}>

View File

@@ -91,7 +91,10 @@
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<div
class="rounded-md border min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto"
bind:this={scrollContainer}
>
<Table.Root>
<Table.Header class="bg-background">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}

View File

@@ -99,9 +99,13 @@
</script>
<div class="w-full">
<div class="max-h-[600px] overflow-y-auto rounded-md border" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div
class="catalog-table-shell min-h-[360px] max-h-[calc(100svh-420px)] overflow-auto [&_[data-slot=table-container]]:overflow-visible [&_[data-slot=table-container]]:w-full"
bind:this={scrollContainer}
>
<div class="min-w-max">
<Table.Root>
<Table.Header class="catalog-table-header">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
{@const headerList = headerGroup.headers}
{@const lastHeaderColId = headerList[headerList.length - 1]?.column.id}
@@ -111,9 +115,9 @@
<Table.Head
class={[
colId === 'select' &&
'sticky left-0 z-40 min-w-[2.75rem] border-r border-border/70 bg-background shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]',
'catalog-table-sticky-left z-40 min-w-[2.75rem]',
colId === lastHeaderColId &&
'sticky right-0 z-40 border-l border-border/70 bg-background shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.35)]'
'catalog-table-sticky-right z-40'
]
.filter(Boolean)
.join(' ')}
@@ -192,9 +196,7 @@
{@const lastCellColId = visibleCells[visibleCells.length - 1]?.column.id}
<Table.Row
data-state={row.getIsSelected() && 'selected'}
class="group/inv-list cursor-pointer transition-colors {row.getIsSelected()
? 'bg-gray-300 dark:bg-gray-600'
: 'hover:bg-gray-100 dark:hover:bg-gray-700'}"
class="group/inv-list cursor-pointer {row.getIsSelected() ? 'catalog-table-row-selected' : 'catalog-table-row'}"
onclick={() => onRowClick && onRowClick(row.original)}
>
{#each visibleCells as cell (cell.id)}
@@ -202,12 +204,12 @@
<Table.Cell
class={[
colId === 'select' &&
'sticky left-0 z-30 min-w-[2.75rem] border-r border-border/70 shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]',
'catalog-table-sticky-left z-30 min-w-[2.75rem]',
colId === lastCellColId &&
'sticky right-0 z-30 border-l border-border/70 shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.35)]',
'catalog-table-sticky-right z-30',
row.getIsSelected()
? 'bg-gray-300 dark:bg-gray-600'
: 'bg-background group-hover/inv-list:bg-gray-100 dark:group-hover/inv-list:bg-gray-700'
? 'catalog-table-sticky-row-selected'
: 'catalog-table-sticky-row-hover'
]
.filter(Boolean)
.join(' ')}
@@ -244,6 +246,7 @@
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</Table.Root>
</div>
</div>
</div>

View File

@@ -97,16 +97,25 @@
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-auto" bind:this={scrollContainer}>
<Table.Root class="w-full">
<Table.Header class="bg-background sticky top-0 z-10">
<div
class="min-h-[360px] max-h-[calc(100svh-420px)] overflow-auto rounded-md border bg-background [&_[data-slot=table-container]]:overflow-visible [&_[data-slot=table-container]]:w-full"
bind:this={scrollContainer}
>
<div class="min-w-max">
<Table.Root>
<Table.Header class="sticky top-0 z-20 border-b bg-background/95 shadow-sm backdrop-blur-md">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head class="whitespace-nowrap p-0 {header.column.columnDef.meta?.className || ''}">
{@const colId = header.column.id}
<Table.Head
class="whitespace-nowrap text-sm font-semibold text-foreground/90 p-0 {header.column.columnDef.meta?.className || ''} {colId === 'select'
? 'sticky left-0 z-40 min-w-[2.75rem] border-r border-border/70 bg-background shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]'
: ''}"
>
{#if !header.isPlaceholder}
<button
class="group flex h-full w-full items-center gap-2 px-2 py-2 text-left hover:bg-muted/50"
class="group flex h-full w-full items-center justify-between gap-2 px-2 py-2 text-left {header.column.getCanSort() ? 'cursor-pointer select-none' : ''}"
onclick={header.column.getToggleSortingHandler()}
disabled={!header.column.getCanSort()}
>
@@ -178,10 +187,19 @@
}
}}
ondblclick={() => handleRowDoubleClick(row)}
class="cursor-pointer hover:bg-muted/50 transition-colors {row.getIsSelected() ? 'bg-primary/10' : ''}"
class="group/inv-list cursor-pointer transition-colors {row.getIsSelected()
? 'bg-gray-300 dark:bg-gray-600'
: 'hover:bg-gray-100 dark:hover:bg-gray-700'}"
>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell class="whitespace-nowrap {cell.column.columnDef.meta?.className || ''}">
{@const colId = cell.column.id}
<Table.Cell
class="whitespace-nowrap {cell.column.columnDef.meta?.className || ''} {colId === 'select'
? 'sticky left-0 z-30 min-w-[2.75rem] border-r border-border/70 bg-background shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]'
: ''} {row.getIsSelected()
? 'bg-gray-300 dark:bg-gray-600'
: 'bg-background group-hover/inv-list:bg-gray-100 dark:group-hover/inv-list:bg-gray-700'}"
>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
@@ -217,6 +235,7 @@
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</Table.Root>
</div>
</div>
</div>

View File

@@ -12,19 +12,6 @@ export type CodePedimentoRegimen = {
export function createColumns(onSuccess?: () => void): ColumnDef<CodePedimentoRegimen>[] {
return [
{
accessorKey: "id",
header: "ID",
cell: ({ row }) => {
const idSnippet = createRawSnippet<[{ id: number }]>((getId) => {
const { id } = getId();
return {
render: () => `<div class="font-medium">${id}</div>`
};
});
return renderSnippet(idSnippet, { id: row.original.id });
}
},
{
accessorKey: "pedimento_code",
header: "Código Pedimento",

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { CodePedimentoRegimen } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.id.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { Container } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.key.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { Country } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.m3_key.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { CurrencyType } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.code.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { CustomsSection } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.customs_code.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { CustomsWarehouse } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(`${item.key}|${item.customs}`);
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { Incoterm } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.code.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -3,104 +3,114 @@
type ColumnDef,
getCoreRowModel
} from "@tanstack/table-core";
import { onMount } from "svelte";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
loading: boolean;
hasMore: boolean;
loadMore: () => void;
};
let {
data,
columns,
pageCount,
totalItems
loading,
hasMore,
loadMore
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() { return data; },
get columns() { return columns; },
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
get pageCount() { return pageCount; },
getCoreRowModel: getCoreRowModel()
});
function handlePageChange(newPage: number) {
const url = new URL($page.url);
url.searchParams.set('page', newPage.toString());
goto(url, { keepFocus: true, noScroll: true });
}
let scrollContainer = $state<HTMLDivElement>();
let loadingTrigger = $state<HTMLDivElement>();
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
onMount(() => {
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting && hasMore && !loading) {
loadMore();
}
},
{ root: scrollContainer, threshold: 0.1 }
);
if (loadingTrigger) observer.observe(loadingTrigger);
return () => observer.disconnect();
});
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<tr>
{#each headerGroup.headers as header (header.id)}
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</th>
{/each}
</tr>
{/each}
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4 px-2">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems} registros
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(currentPage - 1)}
disabled={currentPage <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(currentPage + 1)}
disabled={currentPage >= pageCount}
>
Siguiente
</Button>
</td>
{/each}
</tr>
{:else}
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</td>
</tr>
{/each}
<!-- Loading Trigger -->
{#if hasMore}
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</td>
</tr>
{/if}
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { InvoiceType } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.key.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { MaterialType } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.key.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { PaymentMethod } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.key.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { PedimentoCode } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.code.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { PedimentoRegimen } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.code.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -25,6 +25,19 @@
let loading = $state(false);
let error = $state<string | null>(null);
function getActiveCompanyId(): number | null {
const fromStore = companyStore.activeCompany?.id;
if (fromStore) return fromStore;
if (typeof document === 'undefined') return null;
const cookie = document.cookie
.split('; ')
.find((row) => row.startsWith('active_company_id='))
?.split('=')[1];
if (!cookie) return null;
const parsed = Number(cookie);
return Number.isFinite(parsed) ? parsed : null;
}
// Actualizar formData cuando item cambia
$effect(() => {
if (item) {
@@ -49,7 +62,7 @@
loading = true;
error = null;
const companyId = companyStore.activeCompany?.id;
const companyId = getActiveCompanyId();
if (!companyId) {
error = 'No hay empresa activa seleccionada';
loading = false;

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -18,10 +18,23 @@
let loading = $state(false);
let error = $state<string | null>(null);
function getActiveCompanyId(): number | null {
const fromStore = companyStore.activeCompany?.id;
if (fromStore) return fromStore;
if (typeof document === 'undefined') return null;
const cookie = document.cookie
.split('; ')
.find((row) => row.startsWith('active_company_id='))
?.split('=')[1];
if (!cookie) return null;
const parsed = Number(cookie);
return Number.isFinite(parsed) ? parsed : null;
}
async function handleDelete() {
if (!item) return;
const companyId = companyStore.activeCompany?.id;
const companyId = getActiveCompanyId();
if (!companyId) {
error = 'No hay empresa activa seleccionada';
return;

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { TransportMode } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.key.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { TransportType } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.transport_code.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -59,65 +59,67 @@
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -3,9 +3,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { ValuationMethod } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
@@ -16,8 +14,6 @@
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.key.toString());
@@ -26,14 +22,6 @@
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -54,13 +42,8 @@
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -60,65 +60,67 @@
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<table class="w-full caption-bottom text-sm border-collapse">
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
<tr>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
</th>
{/each}
</Table.Row>
</tr>
{/each}
</Table.Header>
<Table.Body>
</thead>
<tbody class="[&_tr:last-child]:border-0">
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</td>
{/each}
</Table.Row>
</tr>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
<tr>
<td colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
</td>
</tr>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
<!-- Loading Trigger -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
<tr>
<td colspan={columns.length} class="h-24 text-center p-0">
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
<div class="text-muted-foreground text-sm flex items-center gap-2">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
</td>
</tr>
{/if}
</Table.Body>
</Table.Root>
</tbody>
</table>
</div>
</div>

View File

@@ -18,8 +18,8 @@
let { data, columns, loading = false, hasMore = false, loadMore }: Props = $props();
let scrollContainer: HTMLDivElement;
let observer: IntersectionObserver;
let scrollContainer = $state<HTMLDivElement>();
let loadingTrigger = $state<HTMLDivElement>();
let options = $derived<TableOptions<TData>>({
get data() {
@@ -34,8 +34,7 @@
onMount(() => {
if (!loadMore) return;
// Create intersection observer for infinite scroll
observer = new IntersectionObserver(
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting && hasMore && !loading && loadMore) {
@@ -48,26 +47,24 @@
}
);
// Observe the last row
const lastRow = scrollContainer?.querySelector('tbody tr:last-child');
if (lastRow) {
observer.observe(lastRow);
if (loadingTrigger) {
observer.observe(loadingTrigger);
}
return () => {
observer?.disconnect();
observer.disconnect();
};
});
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<div class="flex h-full w-full flex-col overflow-hidden">
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
<Table.Header class="sticky top-0 z-20 border-b bg-background/95 shadow-sm backdrop-blur-md">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<Table.Head class="whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
@@ -83,7 +80,7 @@
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<Table.Cell class="whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
@@ -98,11 +95,24 @@
</Table.Cell>
</Table.Row>
{/each}
{#if loading}
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-12 text-center text-muted-foreground">
Cargando...
<Table.Cell colspan={columns.length} class="h-24 p-0 text-center">
<div bind:this={loadingTrigger} class="flex h-full w-full items-center justify-center">
{#if loading}
<div class="flex items-center justify-center gap-3 rounded-full border bg-muted/30 px-6 py-2 shadow-sm">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
<span class="text-sm font-medium text-foreground">Cargando más registros...</span>
</div>
{:else}
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<span class="h-px w-8 bg-border"></span>
<span>Desplázate para cargar más</span>
<span class="h-px w-8 bg-border"></span>
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
{/if}

View File

@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="sidebar-inset"
class={cn(
"bg-background relative flex w-full flex-1 flex-col",
"bg-background relative flex min-h-0 w-full flex-1 flex-col overflow-hidden",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm",
className
)}

View File

@@ -42,7 +42,7 @@
data-slot="sidebar-wrapper"
style="--sidebar-width: {SIDEBAR_WIDTH}; --sidebar-width-icon: {SIDEBAR_WIDTH_ICON}; {style}"
class={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex h-svh w-full overflow-hidden",
className
)}
bind:this={ref}

View File

@@ -4,19 +4,24 @@ import type { ShortcutDef } from '$lib/stores/shortcut-store';
* Reusable shortcuts for simple reference data catalogs.
*/
export const obtenerAtajosReferenceDataSimple = (acciones: {
manejarNuevo: () => void;
manejarNuevo?: () => void;
manejarActualizar: () => void;
}): ShortcutDef[] => {
return [
{
const atajos: ShortcutDef[] = [];
if (acciones.manejarNuevo) {
atajos.push({
key: 'Alt+Shift+N',
description: 'Nuevo Registro',
action: acciones.manejarNuevo
},
{
key: 'Alt+Shift+R',
description: 'Actualizar Lista',
action: acciones.manejarActualizar
}
];
});
}
atajos.push({
key: 'Alt+Shift+R',
description: 'Actualizar Lista',
action: acciones.manejarActualizar
});
return atajos;
};

View File

@@ -141,7 +141,7 @@
-->
</div>
</header>
<div class="flex flex-1 flex-col min-h-0 gap-4 overflow-x-hidden p-4 pt-0">
<div class="flex flex-1 flex-col min-h-0 gap-4 overflow-y-auto overflow-x-hidden p-4 pt-0">
{#if csvImportBanner}
<a
href="/dashboard/csv-upload"

View File

@@ -43,7 +43,7 @@
}
</script>
<div class="container mx-auto flex min-h-[calc(100vh-85px)] flex-col space-y-6 py-6">
<div class="flex flex-col gap-6 overflow-hidden h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">{m['sidebar.audit_logs_title']()}</h1>

View File

@@ -8,7 +8,6 @@
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 * as Table from '$lib/components/ui/table';
import { RefreshCw, Search } from 'lucide-svelte';
@@ -153,106 +152,78 @@
</script>
<div class="flex min-h-0 flex-1 flex-col gap-4">
<div class="flex flex-none items-center justify-end">
<Button
variant="outline"
onclick={() => {
page = 1;
hasMore = true;
logs = [];
void loadLogs();
}}
disabled={loading}
>
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
Actualizar
</Button>
</div>
<div class="flex-none">
<Card.Root>
<Card.Header class="py-3">
<Card.Title class="text-lg">Filtros</Card.Title>
</Card.Header>
<Card.Content>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 lg:grid-cols-5">
<div class="space-y-1">
<Label for="search" class="text-xs">Búsqueda General</Label>
<div class="relative">
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
<Input
id="search"
placeholder="Ref, Mov, Usuario..."
class="h-9 pl-8"
bind:value={search}
oninput={handleSearchInput}
/>
</div>
</div>
<div class="space-y-1">
<Label for="username" class="text-xs">Usuario</Label>
<Input
id="username"
placeholder="Filtrar por usuario"
class="h-9"
bind:value={usernameFilter}
oninput={handleSearchInput}
/>
</div>
<div class="space-y-1">
<Label for="procedure" class="text-xs">Procedimiento</Label>
<select
id="procedure"
bind:value={procedureFilter}
onchange={handleFilterChange}
class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">Todos</option>
{#each procedures as proc}
<option value={proc}>{proc}</option>
{/each}
</select>
</div>
<div class="space-y-1">
<Label for="dateFrom" class="text-xs">Desde</Label>
<Input
id="dateFrom"
type="date"
class="h-9"
bind:value={dateFrom}
onchange={handleFilterChange}
/>
</div>
<div class="space-y-1">
<Label for="dateTo" class="text-xs">Hasta</Label>
<Input
id="dateTo"
type="date"
class="h-9"
bind:value={dateTo}
onchange={handleFilterChange}
/>
</div>
</div>
<div class="mt-2 flex justify-end">
<Button variant="ghost" onclick={clearFilters} size="sm" class="h-8 text-xs font-normal">
Limpiar Filtros
</Button>
</div>
</Card.Content>
</Card.Root>
</div>
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden">
<Card.Header class="flex-none py-3">
<div class="flex items-center justify-between">
<div>
<Card.Title class="text-lg">Registros</Card.Title>
<Card.Description class="text-xs">Total: {total} registros encontrados</Card.Description>
<Card.Title>Bitácora de Auditoría</Card.Title>
<Card.Description class="text-xs">Mostrando {logs.length} de {total} registros</Card.Description>
</div>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
<Input
id="search"
placeholder="Ref, Mov, Usuario..."
class="h-9 w-48 pl-8"
bind:value={search}
oninput={handleSearchInput}
/>
</div>
<Input
id="username"
placeholder="Usuario"
class="h-9 w-32"
bind:value={usernameFilter}
oninput={handleSearchInput}
/>
<select
id="procedure"
bind:value={procedureFilter}
onchange={handleFilterChange}
class="flex h-9 w-[180px] rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
title="Procedimiento"
>
<option value="">Procedimiento: Todos</option>
{#each procedures as proc}
<option value={proc}>{proc}</option>
{/each}
</select>
<Input
id="dateFrom"
type="date"
class="h-9 w-36"
bind:value={dateFrom}
onchange={handleFilterChange}
title="Desde"
/>
<span class="text-sm text-muted-foreground">-</span>
<Input
id="dateTo"
type="date"
class="h-9 w-36"
bind:value={dateTo}
onchange={handleFilterChange}
title="Hasta"
/>
<Button variant="outline" size="sm" class="h-9" onclick={clearFilters}>
Limpiar
</Button>
<Button
variant="outline"
size="sm"
class="h-9"
onclick={() => {
page = 1;
hasMore = true;
logs = [];
void loadLogs();
}}
disabled={loading}
>
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
Actualizar
</Button>
</div>
</div>
</Card.Header>

View File

@@ -80,58 +80,53 @@
</script>
<div class="flex min-h-0 flex-1 flex-col gap-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="text-lg font-semibold">{m['sidebar.audit_logs_files_title']()}</h2>
<p class="text-sm text-muted-foreground">{displayPath || m['sidebar.audit_logs_files_root']()}</p>
</div>
<Button variant="outline" onclick={() => void loadPath(currentPath)} disabled={loading}>
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
{m['sidebar.audit_logs_files_refresh']()}
</Button>
</div>
<Card.Root>
<Card.Content class="py-2">
<div class="flex flex-wrap items-center gap-1 text-xs text-muted-foreground">
{#each breadcrumbs as crumb, idx}
{#if idx > 0}
<span class="mx-1 text-[10px]">/</span>
{/if}
{#if idx === breadcrumbs.length - 1}
<span
class="max-w-[240px] truncate font-medium text-foreground sm:max-w-[320px]"
>
{crumb.display_name}
</span>
{:else}
<button
type="button"
class="max-w-[180px] truncate hover:text-foreground hover:underline underline-offset-2"
onclick={() => void loadPath(crumb.path)}
disabled={loading}
>
{crumb.display_name}
</button>
{/if}
{/each}
</div>
</Card.Content>
</Card.Root>
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden">
<Card.Header class="pb-2">
<Card.Title class="text-base">{m['sidebar.audit_logs_files_list_title']()}</Card.Title>
<Card.Header class="flex-none py-3">
<div class="flex items-center justify-between">
<div>
<Card.Title>{m['sidebar.audit_logs_files_title']()}</Card.Title>
<Card.Description class="text-xs">
<div class="mt-1 flex flex-wrap items-center gap-1 text-xs text-muted-foreground">
{#each breadcrumbs as crumb, idx}
{#if idx > 0}
<span class="mx-0.5 text-[10px]">/</span>
{/if}
{#if idx === breadcrumbs.length - 1}
<span class="max-w-[240px] truncate font-medium text-foreground sm:max-w-[320px]">
{crumb.display_name}
</span>
{:else}
<button
type="button"
class="max-w-[180px] truncate hover:text-foreground hover:underline underline-offset-2"
onclick={() => void loadPath(crumb.path)}
disabled={loading}
>
{crumb.display_name}
</button>
{/if}
{/each}
{#if breadcrumbs.length === 0}
{m['sidebar.audit_logs_files_root']()}
{/if}
</div>
</Card.Description>
</div>
<Button variant="outline" size="sm" class="h-9" onclick={() => void loadPath(currentPath)} disabled={loading}>
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
{m['sidebar.audit_logs_files_refresh']()}
</Button>
</div>
</Card.Header>
<Card.Content class="min-h-0 flex-1 overflow-auto p-0">
<Card.Content class="flex min-h-0 flex-1 flex-col p-0 px-6 pb-6">
{#if error}
<div class="m-4 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300">
<div class="flex-none m-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300">
{m['sidebar.audit_logs_files_error_prefix']()} {error}
</div>
{/if}
<div class="w-full overflow-x-auto">
<div class="relative min-h-0 flex-1 overflow-auto rounded-md border bg-card shadow-inner">
<Table.Root class="min-w-full text-sm">
<Table.Header>
<Table.Header class="sticky top-0 z-10 bg-background/95 shadow-sm backdrop-blur-sm">
<Table.Row>
<Table.Head class="min-w-[200px]">
{m['sidebar.audit_logs_files_col_name']()}
@@ -163,7 +158,7 @@
{:else}
{#each folders as folder}
<Table.Row
class="cursor-pointer hover:bg-muted/30"
class="cursor-pointer transition-colors hover:bg-muted/50"
onclick={() => void loadPath(folder.path)}
>
<Table.Cell class="max-w-[260px] font-medium">
@@ -178,7 +173,7 @@
</Table.Row>
{/each}
{#each files as file}
<Table.Row>
<Table.Row class="transition-colors hover:bg-muted/50">
<Table.Cell class="max-w-[260px] font-medium">
<span class="truncate">{file.display_name}</span>
</Table.Cell>

View File

@@ -6,9 +6,9 @@
type UnifiedTaskStatus
} from '$lib/api/dashboard/a76/tasks';
import { companyStore } from '$lib/stores/company.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import * as Select from '$lib/components/ui/select';
import * as Dialog from '$lib/components/ui/dialog';
import { Badge } from '$lib/components/ui/badge';
@@ -146,106 +146,116 @@
});
</script>
<div class="flex min-h-0 flex-1 flex-col space-y-4">
<div class="flex flex-none items-center justify-end">
<Button onclick={() => void loadTasks(true)} disabled={loading}>
{loading ? 'Cargando...' : 'Refrescar'}
</Button>
</div>
<div class="flex min-h-0 flex-1 flex-col gap-4">
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden">
<Card.Header class="flex-none py-3">
<div class="flex items-center justify-between">
<div>
<Card.Title>Tareas del Sistema</Card.Title>
<Card.Description class="text-xs">Total: {total} tareas encontradas</Card.Description>
</div>
<div class="flex flex-wrap items-center gap-2">
<Input placeholder="Buscar por task_id / nombre / error" bind:value={search} class="h-9 w-64" />
<select
bind:value={statusFilter}
class="flex h-9 w-[160px] rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
title="Estado"
>
<option value="all">Estado: Todos</option>
<option value="pending">En cola</option>
<option value="active">En progreso</option>
<option value="completed">Completadas</option>
<option value="failed">Fallidas</option>
</select>
<Button variant="outline" size="sm" class="h-9" onclick={applyFilters}>Aplicar Filtros</Button>
<Button size="sm" class="h-9" onclick={() => void loadTasks(true)} disabled={loading}>
{loading ? 'Cargando...' : 'Refrescar'}
</Button>
</div>
</div>
</Card.Header>
<Card.Content class="flex min-h-0 flex-1 flex-col p-0 px-6 pb-3">
{#if error}
<div class="flex-none rounded border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300">
{error}
</div>
{/if}
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
<Input placeholder="Buscar por task_id / nombre / error" bind:value={search} />
<Select.Root type="single" bind:value={statusFilter}>
<Select.Trigger><Select.Value placeholder="Estado" /></Select.Trigger>
<Select.Content>
<Select.Item value="all">Todos</Select.Item>
<Select.Item value="pending">En cola</Select.Item>
<Select.Item value="active">En progreso</Select.Item>
<Select.Item value="completed">Completadas</Select.Item>
<Select.Item value="failed">Fallidas</Select.Item>
</Select.Content>
</Select.Root>
<Button variant="outline" onclick={applyFilters}>Aplicar filtros</Button>
</div>
{#if error}
<div
class="flex-none rounded border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300"
>
{error}
</div>
{/if}
<div class="min-h-0 flex-1 overflow-auto rounded-md border">
<table class="w-full text-sm">
<thead class="bg-muted/40">
<tr>
<th class="p-2 text-left">Task ID</th>
<th class="p-2 text-left">Tipo</th>
<th class="p-2 text-left">Estado</th>
<th class="p-2 text-left">Progreso</th>
<th class="p-2 text-left">Reintentos</th>
<th class="p-2 text-left">Actualizado</th>
</tr>
</thead>
<tbody>
{#if tasks.length === 0 && !loading}
<tr>
<td class="p-4 text-center text-muted-foreground" colspan="6">Sin tareas registradas</td>
</tr>
{:else if tasks.length === 0 && loading}
<tr>
<td class="p-4 text-center text-muted-foreground italic" colspan="6">Cargando...</td>
</tr>
{:else}
{#each tasks as task}
<tr
class="cursor-pointer border-t hover:bg-muted/20 {selectedDetail?.task_id === task.task_id
? 'bg-muted/30'
: ''}"
onclick={() => void openDetail(task)}
>
<td class="p-2 font-mono text-xs">{task.task_id}</td>
<td class="p-2">{task.task_group} / {task.task_name}</td>
<td class={`p-2 font-medium ${statusClass(task.status)}`}>
{statusLabel(task.status)}
<span class="ml-1 text-xs font-normal text-muted-foreground"
>({task.celery_state_raw})</span
>
</td>
<td class="p-2">
{formatPercent(task)}
{task.progress?.message ? ` · ${task.progress.message}` : ''}
</td>
<td class="p-2">{task.retries ?? 0}</td>
<td class="p-2 whitespace-nowrap">{new Date(task.updated_at).toLocaleString()}</td>
<div class="relative min-h-0 flex-1 overflow-auto rounded-md border bg-card shadow-inner">
<table class="w-full text-sm">
<thead class="sticky top-0 z-10 bg-background/95 shadow-sm backdrop-blur-sm">
<tr>
<th class="p-2 text-left">Task ID</th>
<th class="p-2 text-left">Tipo</th>
<th class="p-2 text-left">Estado</th>
<th class="p-2 text-left">Progreso</th>
<th class="p-2 text-left">Reintentos</th>
<th class="p-2 text-left">Actualizado</th>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
<div class="flex flex-none items-center justify-between text-sm">
<div class="text-muted-foreground">Total: {total}</div>
<div class="flex items-center gap-2">
<Button
variant="outline"
disabled={page <= 1 || loading}
onclick={() => {
page -= 1;
void loadTasks(false);
}}>Anterior</Button>
<span>Página {page}</span>
<Button
variant="outline"
disabled={page * pageSize >= total || loading}
onclick={() => {
page += 1;
void loadTasks(false);
}}>Siguiente</Button>
</thead>
<tbody>
{#if tasks.length === 0 && !loading}
<tr>
<td class="p-4 text-center text-muted-foreground" colspan="6">Sin tareas registradas</td>
</tr>
{:else if tasks.length === 0 && loading}
<tr>
<td class="p-4 text-center text-muted-foreground italic" colspan="6">Cargando...</td>
</tr>
{:else}
{#each tasks as task}
<tr
class="cursor-pointer border-t transition-colors hover:bg-muted/50 {selectedDetail?.task_id === task.task_id
? 'bg-muted/30'
: ''}"
onclick={() => void openDetail(task)}
>
<td class="p-2 font-mono text-xs">{task.task_id}</td>
<td class="p-2">{task.task_group} / {task.task_name}</td>
<td class={`p-2 font-medium ${statusClass(task.status)}`}>
{statusLabel(task.status)}
<span class="ml-1 text-xs font-normal text-muted-foreground"
>({task.celery_state_raw})</span
>
</td>
<td class="p-2">
{formatPercent(task)}
{task.progress?.message ? ` · ${task.progress.message}` : ''}
</td>
<td class="p-2">{task.retries ?? 0}</td>
<td class="p-2 whitespace-nowrap">{new Date(task.updated_at).toLocaleString()}</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
</Card.Content>
<div class="flex flex-none items-center justify-between px-6 pb-4 text-sm">
<div class="text-muted-foreground">Total: {total}</div>
<div class="flex items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={page <= 1 || loading}
onclick={() => {
page -= 1;
void loadTasks(false);
}}>Anterior</Button
>
<span>Página {page}</span>
<Button
variant="outline"
size="sm"
disabled={page * pageSize >= total || loading}
onclick={() => {
page += 1;
void loadTasks(false);
}}>Siguiente</Button
>
</div>
</div>
</div>
</Card.Root>
</div>
<Dialog.Root

View File

@@ -187,15 +187,15 @@
const brokerColumns = createBrokerColumns(handleActionSuccess);
</script>
<div class="flex h-[calc(100vh-4rem)] flex-col gap-4 p-4 pb-15">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-1">
<h1 class="text-2xl font-bold">GESTIÓN ADUANAL</h1>
<p class="text-sm text-muted-foreground">Administración de Agentes y Secciones Aduanales</p>
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-2xl font-bold tracking-tight">Gestión Aduanal</h1>
<p class="text-muted-foreground">Administración de Agentes y Secciones Aduanales</p>
</div>
</div>
<Tabs.Root bind:value={activeTab} class="flex flex-1 flex-col overflow-hidden">
<Tabs.Root bind:value={activeTab} class="flex min-h-0 flex-1 flex-col overflow-hidden">
<Tabs.List class="mb-4 w-full justify-start rounded-none border-b bg-transparent p-0">
<Tabs.Trigger
value="brokers"
@@ -215,8 +215,8 @@
value="brokers"
class="mt-0 flex flex-1 gap-4 overflow-hidden data-[state=inactive]:hidden"
>
<div class="flex flex-1 flex-col gap-4 overflow-hidden">
<div class="rounded-lg border bg-card">
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden">
<div class="rounded-md border bg-background">
<div class="space-y-4 p-4">
<div class="flex items-center justify-between">
<h2 class="text-sm font-semibold">Filtros</h2>
@@ -228,7 +228,7 @@
<Input
bind:value={searchName}
placeholder="Buscar por nombre..."
class="h-9"
class="h-9 bg-card"
oninput={handleSearch}
/>
</div>
@@ -237,7 +237,7 @@
<Input
bind:value={searchPatent}
placeholder="Num. Patente..."
class="h-9"
class="h-9 bg-card"
oninput={handleSearch}
/>
</div>
@@ -246,8 +246,8 @@
</div>
</div>
<div class="flex flex-1 flex-col overflow-hidden rounded-lg border">
<div class="flex items-center justify-between border-b bg-muted/30 p-3">
<div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border bg-background">
<div class="flex items-center justify-between border-b bg-background/95 p-3">
<h2 class="text-sm font-semibold">Listado</h2>
<div class="flex items-center gap-2">
<span class="text-xs text-muted-foreground">
@@ -260,7 +260,7 @@
</div>
</div>
<div class="flex-1 overflow-auto bg-card">
<div class="flex-1 overflow-auto bg-background">
<BrokerDataTable
data={paginatedItems}
columns={brokerColumns}
@@ -295,9 +295,7 @@
</div>
</div>
<div
class="flex w-96 flex-none flex-col overflow-hidden rounded-xl border bg-muted/30 shadow-sm"
>
<div class="flex w-96 flex-none flex-col overflow-hidden rounded-xl border bg-muted/30 shadow-sm">
<div class="border-b bg-card p-4">
<p class="text-[10px] tracking-widest text-muted-foreground uppercase opacity-80">
Detalles del Agente
@@ -407,6 +405,8 @@
</Card.Root>
</Tabs.Content>
</Tabs.Root>
<div class="h-20"></div>
</div>
<div
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"

View File

@@ -1,18 +1,22 @@
<script lang="ts">
import { page } from '$app/stores';
import DataTable from '$lib/components/dashboard/general_catalogs/classification_concepts/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { createColumns } from '$lib/components/dashboard/general_catalogs/classification_concepts/columns';
import CreateDialog from '$lib/components/dashboard/general_catalogs/classification/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaClasificacion } from '$lib/config/shortcuts/dashboard/general_catalogs/classification/list';
import { getClassificationConcepts } from '$lib/api/dashboard/a76/general_catalogs/classification-concepts';
import { companyStore } from '$lib/stores/company.svelte';
let { data } = $props();
let dialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Atajos
useShortcuts(
@@ -28,17 +32,54 @@
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
const columns = $derived(createColumns(handleSuccess));
function handleSearch() {
let allItems = $state(data.classifications?.items || []);
let currentPage = $state(data.classifications?.page || 1);
let pageSize = $state(data.classifications?.page_size || 50);
let totalItems = $state(data.classifications?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.classifications) {
allItems = data.classifications.items || [];
currentPage = data.classifications.page || 1;
totalItems = data.classifications.total || 0;
pageSize = data.classifications.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchClassification) filters.classification = searchClassification;
if (searchDesc) filters.description = searchDesc;
const response = await getClassificationConcepts(
1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchClassification) url.searchParams.set('classification', searchClassification);
else url.searchParams.delete('classification');
@@ -46,47 +87,131 @@
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchClassification) filters.classification = searchClassification;
if (searchDesc) filters.description = searchDesc;
const response = await getClassificationConcepts(
currentPage + 1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage += 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more classifications:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchClassification) filters.classification = searchClassification;
if (searchDesc) filters.description = searchDesc;
const response = await getClassificationConcepts(
1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading classifications:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Clasificaciones de Conceptos</h1>
<p class="text-muted-foreground">Gestión del catálogo de clasificaciones de conceptos</p>
</div>
<Button onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nueva Clasificación
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por clasificación..."
bind:value={searchClassification}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar
</Button>
<Button class="h-9" onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nueva Clasificación
</Button>
</div>
</div>
<DataTable
data={data.classifications?.items || []}
{columns}
pageCount={data.classifications?.pages || 0}
totalItems={data.classifications?.total || 0}
/>
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<Card.Root class="border bg-background flex flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Clasificaciones</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<Input
placeholder="Clasificación"
bind:value={searchClassification}
oninput={handleSearch}
class="h-9 w-40 bg-card lg:w-52"
/>
<Input
placeholder="Descripción"
bind:value={searchDesc}
oninput={handleSearch}
class="h-9 w-44 bg-card lg:w-64"
/>
</div>
</div>
</Card.Header>
<Card.Content class="p-0">
<div class="rounded-md border bg-background overflow-hidden">
<InfiniteDataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">
Mostrando {allItems.length} de {totalItems} registros
</div>
<CreateDialog
bind:open={dialogOpen}
onSuccess={handleSuccess}

View File

@@ -1,17 +1,19 @@
<script lang="ts">
import { page } from '$app/stores';
import DataTable from '$lib/components/dashboard/general_catalogs/company/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/company/columns';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import * as m from '$lib/paraglide/messages.js';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaEmpresa } from '$lib/config/shortcuts/dashboard/general_catalogs/company_information/list';
import { getCompanies } from '$lib/api/dashboard/a76/general_catalogs/company';
let { data } = $props();
@@ -24,16 +26,53 @@
})
);
let dialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Filtros
let searchName = $state($page.url.searchParams.get('name') || '');
let searchRfc = $state($page.url.searchParams.get('rfc') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
let allItems = $state(data.companies?.items || []);
let currentPage = $state(data.companies?.page || 1);
let pageSize = $state(data.companies?.page_size || 50);
let totalItems = $state(data.companies?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.companies) {
allItems = data.companies.items || [];
currentPage = data.companies.page || 1;
totalItems = data.companies.total || 0;
pageSize = data.companies.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchName) filters.name = searchName;
if (searchRfc) filters.rfc = searchRfc;
const response = await getCompanies(1, pageSize, filters);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchName) url.searchParams.set('name', searchName);
else url.searchParams.delete('name');
@@ -41,44 +80,118 @@
if (searchRfc) url.searchParams.set('rfc', searchRfc);
else url.searchParams.delete('rfc');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchName) filters.name = searchName;
if (searchRfc) filters.rfc = searchRfc;
const response = await getCompanies(currentPage + 1, pageSize, filters);
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage += 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more companies:', err);
} finally {
loading = false;
}
}
async function reloadData() {
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchName) filters.name = searchName;
if (searchRfc) filters.rfc = searchRfc;
const response = await getCompanies(1, pageSize, filters);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading companies:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Información de Empresas</h1>
<p class="text-muted-foreground">Gestión de información de empresas</p>
</div>
<Button href="/dashboard/general_catalogs/company_information/edit">
<Plus class="mr-2 h-4 w-4" />
Nueva Empresa
</Button>
</div>
<div class="flex items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por nombre..." bind:value={searchName} oninput={handleSearch} />
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por RFC..." bind:value={searchRfc} oninput={handleSearch} />
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar
</Button>
<Button class="h-9" href="/dashboard/general_catalogs/company_information/edit">
<Plus class="mr-2 h-4 w-4" />
Nueva Empresa
</Button>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.companies?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.companies?.pages || 0}
totalItems={data.companies?.total || 0}
/>
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<Card.Root class="border bg-background flex flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Empresas</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<Input
placeholder="Nombre"
bind:value={searchName}
oninput={handleSearch}
class="h-9 w-40 bg-card lg:w-56"
/>
<Input
placeholder="RFC"
bind:value={searchRfc}
oninput={handleSearch}
class="h-9 w-36 bg-card lg:w-44"
/>
</div>
</div>
</Card.Header>
<Card.Content class="p-0">
<div class="rounded-md border bg-background overflow-hidden">
<InfiniteDataTable
data={allItems}
columns={createColumns(handleSuccess)}
{loading}
{hasMore}
{loadMore}
/>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">
Mostrando {allItems.length} de {totalItems} registros
</div>
</div>

View File

@@ -1,21 +1,25 @@
<script lang="ts">
import { page } from '$app/stores';
import DataTable from '$lib/components/dashboard/general_catalogs/concepts/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { createColumns } from '$lib/components/dashboard/general_catalogs/concepts/columns';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/concepts/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaConceptos } from '$lib/config/shortcuts/dashboard/general_catalogs/concepts/list';
import { getConcepts } from '$lib/api/dashboard/a76/general_catalogs/concepts';
import { companyStore } from '$lib/stores/company.svelte';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let dialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Atajos
useShortcuts(
@@ -31,10 +35,47 @@
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
let allItems = $state(data.concepts?.items || []);
let currentPage = $state(data.concepts?.page || 1);
let pageSize = $state(data.concepts?.page_size || 50);
let totalItems = $state(data.concepts?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.concepts) {
allItems = data.concepts.items || [];
currentPage = data.concepts.page || 1;
totalItems = data.concepts.total || 0;
pageSize = data.concepts.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getConcepts(1, pageSize, companyStore.activeCompany.id, filters);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
@@ -42,49 +83,125 @@
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getConcepts(
currentPage + 1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage += 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more concepts:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getConcepts(1, pageSize, companyStore.activeCompany.id, filters);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading concepts:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Conceptos</h1>
<p class="text-muted-foreground">Gestión del catálogo de conceptos</p>
</div>
<Button onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Concepto
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar
</Button>
<Button class="h-9" onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Concepto
</Button>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.concepts?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.concepts?.pages || 0}
totalItems={data.concepts?.total || 0}
/>
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<Card.Root class="border bg-background flex flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Conceptos</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<Input
placeholder="Código"
bind:value={searchCode}
oninput={handleSearch}
class="h-9 w-36 bg-card lg:w-44"
/>
<Input
placeholder="Descripción"
bind:value={searchDesc}
oninput={handleSearch}
class="h-9 w-44 bg-card lg:w-64"
/>
</div>
</div>
</Card.Header>
<Card.Content class="p-0">
<div class="rounded-md border bg-background overflow-hidden">
<InfiniteDataTable
data={allItems}
columns={createColumns(handleSuccess)}
{loading}
{hasMore}
{loadMore}
/>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">
Mostrando {allItems.length} de {totalItems} registros
</div>
{#if dialogOpen}

View File

@@ -1,21 +1,24 @@
<script lang="ts">
import { page } from '$app/stores';
import DataTable from '$lib/components/dashboard/general_catalogs/customs_broker_concepts/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { createColumns } from '$lib/components/dashboard/general_catalogs/customs_broker_concepts/columns';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/customs_broker_concepts/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaConceptosAA } from '$lib/config/shortcuts/dashboard/general_catalogs/customs_broker_concepts/list';
import { companyStore } from '$lib/stores/company.svelte';
import { getCustomsBrokerConcepts } from '$lib/api/dashboard/a76/general_catalogs/customs-broker-concepts';
let { data } = $props();
const activeCompanyId = $derived(companyStore.activeCompany?.id);
let dialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Atajos
useShortcuts(
@@ -31,19 +34,54 @@
let searchConcept = $state($page.url.searchParams.get('concept') || '');
let timeout: ReturnType<typeof setTimeout>;
// Solo necesitamos una declaración de handleSuccess
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
// columns depende de handleSuccess, así que se queda igual
const columns = $derived(createColumns(handleSuccess));
function handleSearch() {
let allItems = $state(data.concepts?.items || []);
let currentPage = $state(data.concepts?.page || 1);
let pageSize = $state(data.concepts?.page_size || 50);
let totalItems = $state(data.concepts?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.concepts) {
allItems = data.concepts.items || [];
currentPage = data.concepts.page || 1;
totalItems = data.concepts.total || 0;
pageSize = data.concepts.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchBrokerKey) filters.broker_key = searchBrokerKey;
if (searchConcept) filters.concept = searchConcept;
const response = await getCustomsBrokerConcepts(
1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchBrokerKey) url.searchParams.set('broker_key', searchBrokerKey);
else url.searchParams.delete('broker_key');
@@ -51,48 +89,130 @@
if (searchConcept) url.searchParams.set('concept', searchConcept);
else url.searchParams.delete('concept');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchBrokerKey) filters.broker_key = searchBrokerKey;
if (searchConcept) filters.concept = searchConcept;
const response = await getCustomsBrokerConcepts(
currentPage + 1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage += 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more broker concepts:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchBrokerKey) filters.broker_key = searchBrokerKey;
if (searchConcept) filters.concept = searchConcept;
const response = await getCustomsBrokerConcepts(
1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading broker concepts:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Conceptos de Agente Aduanal</h1>
<p class="text-muted-foreground">Gestión del catálogo de conceptos de agente aduanal</p>
</div>
<Button onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Concepto
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por clave de agente..."
bind:value={searchBrokerKey}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por concepto..."
bind:value={searchConcept}
oninput={handleSearch}
/>
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar
</Button>
<Button class="h-9" onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Concepto
</Button>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.concepts?.items || []}
{columns}
pageCount={data.concepts?.pages || 0}
totalItems={data.concepts?.total || 0}
/>
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<Card.Root class="border bg-background flex flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Conceptos de Agente Aduanal</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<Input
placeholder="Clave agente"
bind:value={searchBrokerKey}
oninput={handleSearch}
class="h-9 w-40 bg-card lg:w-52"
/>
<Input
placeholder="Concepto"
bind:value={searchConcept}
oninput={handleSearch}
class="h-9 w-44 bg-card lg:w-64"
/>
</div>
</div>
</Card.Header>
<Card.Content class="p-0">
<div class="rounded-md border bg-background overflow-hidden">
<InfiniteDataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">
Mostrando {allItems.length} de {totalItems} registros
</div>
{#if activeCompanyId}

View File

@@ -3,7 +3,6 @@
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { getDodas, deleteDoda, type Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
import * as Card from '$lib/components/ui/card';
import { toast } from 'svelte-sonner';
import {
Plus,
@@ -16,6 +15,7 @@
LayoutGrid,
Printer
} from 'lucide-svelte';
import * as Card from '$lib/components/ui/card';
import * as Select from '$lib/components/ui/select';
import { companyStore } from '$lib/stores/company.svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/doda/data-table.svelte';
@@ -23,7 +23,6 @@
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/doda/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Separator } from '$lib/components/ui/separator';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaDoda } from '$lib/config/shortcuts/dashboard/general_catalogs/doda/list';
@@ -194,60 +193,45 @@
}
</script>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">DODA</h1>
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-2xl font-bold tracking-tight">DODA</h1>
<p class="text-muted-foreground">Gestiona tus Documentos de Operación Aduanera (DODA)</p>
</div>
<Button onclick={handleCreateClick} class="shadow-sm transition-all hover:translate-y-[-1px]">
<Plus class="mr-2 h-4 w-4" />
Nuevo DODA
</Button>
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" class="h-9" onclick={reloadData} disabled={loading}>
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
Actualizar
</Button>
<Button class="h-9" onclick={handleCreateClick}>
<Plus class="mr-2 h-4 w-4" />
Nuevo DODA
</Button>
</div>
</div>
<Card.Root>
<Card.Root class="flex min-h-0 flex-1 flex-col border bg-background">
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Filtros Avanzados</Card.Title>
<Card.Description>Refina tu búsqueda mediante múltiples criterios</Card.Description>
</div>
<Button variant="ghost" size="sm" onclick={clearFilters} class="text-muted-foreground">
<RotateCcw class="mr-2 h-4 w-4" />
Limpiar Filtros
</Button>
</div>
</Card.Header>
<Card.Content>
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<div class="space-y-2">
<Label for="search-integration">No. Integración</Label>
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<Card.Title>Listado de DODA</Card.Title>
<div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-[220px_180px_170px_170px_auto] xl:items-center">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="search-integration"
placeholder="Buscar integración..."
placeholder="Folio"
bind:value={filters.integration_number}
class="pl-9"
class="h-9 bg-card pl-9"
/>
</div>
</div>
<div class="space-y-2">
<Label for="search-patent">Patente</Label>
<Input id="search-patent" placeholder="Buscar patente..." bind:value={filters.patent} />
</div>
<div class="space-y-2">
<Label>Estatus</Label>
<Input placeholder="Patente" bind:value={filters.patent} class="h-9 bg-card" />
<Select.Root
type="single"
value={filters.status}
onValueChange={(v) => (filters.status = v)}
>
<Select.Trigger class="w-full">
{filters.status || 'Todos los estatus'}
<Select.Trigger class="h-9 w-full bg-card">
{filters.status || 'Estatus'}
</Select.Trigger>
<Select.Content>
<Select.Item value="">Todos</Select.Item>
@@ -257,21 +241,17 @@
<Select.Item value="ELIMINADO">ELIMINADO</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="space-y-2">
<Label>Operación</Label>
<Select.Root
type="single"
value={filters.operation_type}
onValueChange={(v) => (filters.operation_type = v)}
>
<Select.Trigger class="w-full">
<Select.Trigger class="h-9 w-full bg-card">
{filters.operation_type === 'I'
? 'Importación'
: filters.operation_type === 'E'
? 'Exportación'
: 'Todas'}
: 'Operación'}
</Select.Trigger>
<Select.Content>
<Select.Item value="">Todas</Select.Item>
@@ -279,38 +259,35 @@
<Select.Item value="E">E - Exportación</Select.Item>
</Select.Content>
</Select.Root>
<Button variant="outline" size="sm" class="h-9" onclick={clearFilters}>
<RotateCcw class="mr-2 h-4 w-4" />
Limpiar
</Button>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="rounded-md border bg-background">
<DataTable
data={allItems}
columns={createColumns(reloadData)}
{loading}
{hasMore}
{loadMore}
{selectedId}
onRowClick={handleRowClick}
/>
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de DODAs</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" size="sm" onclick={reloadData} disabled={loading}>
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content class="p-0">
<DataTable
data={allItems}
columns={createColumns(reloadData)}
{loading}
{hasMore}
{loadMore}
{selectedId}
onRowClick={handleRowClick}
/>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">
Mostrando {allItems.length} de {totalItems} registros
<span class="ml-2"></span>
<span class="ml-2">Filtros activos: {Object.values(filters).filter((value) => value !== '').length}</span>
</div>
<div class="h-20"></div>
<!-- Footer fijo de acciones (estilo Facturas) -->
<div
@@ -355,9 +332,6 @@
</div>
</div>
<div class="h-20"></div>
<!-- Espacio buffer para el footer -->
{#if dialogOpen}
<CreateEditDialog bind:open={dialogOpen} onSuccess={reloadData} />
{/if}

View File

@@ -262,7 +262,7 @@
>
<ArrowLeft size={20} />
</Button>
<h1 class="text-3xl font-bold tracking-tight">{title}</h1>
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
</div>
<p class="text-muted-foreground">Catálogos Generales / Doda</p>
</div>

View File

@@ -1,11 +1,12 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import DataTable from '$lib/components/dashboard/transportation/drivers/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import CreateEditDialog from '$lib/components/dashboard/transportation/drivers/create-edit-dialog.svelte';
import { createColumns } from '$lib/components/dashboard/transportation/drivers/columns';
@@ -15,13 +16,12 @@
let data = $state<Driver[]>([]);
let totalItems = $state(0);
let pageCount = $state(0);
let loading = $state(false);
let currentPage = $state(1);
let pageSize = $state(50);
let hasMore = $derived(data.length < totalItems);
let createDialogOpen = $state(false);
let currentPage = $derived(Number($page.url.searchParams.get('page')) || 1);
let pageSize = 10;
let searchTransporterKey = $state($page.url.searchParams.get('transporter_key') || '');
let searchDriverName = $state($page.url.searchParams.get('driver_name') || '');
let searchTimeout: NodeJS.Timeout;
@@ -31,7 +31,7 @@
loading = true;
try {
const params: Record<string, string | number> = {
page: currentPage,
page: 1,
page_size: pageSize
};
// Filtros: el backend aún no los soporta; se mantienen en URL para futura implementación
@@ -42,8 +42,8 @@
if (response.data) {
data = response.data.items;
currentPage = 1;
totalItems = response.data.total;
pageCount = Math.ceil(response.data.total / response.data.page_size) || 1;
}
} catch (error) {
console.error('Error loading drivers:', error);
@@ -52,12 +52,32 @@
}
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
try {
const params: Record<string, string | number> = {
page: currentPage + 1,
page_size: pageSize
};
const response = await driversApi.list(companyStore.activeCompany.id, params);
if (response.data?.items) {
data = [...data, ...response.data.items];
currentPage += 1;
totalItems = response.data.total;
}
} catch (error) {
console.error('Error loading more drivers:', error);
} finally {
loading = false;
}
}
function handleSearch() {
if (!browser) return;
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const url = new URL($page.url);
url.searchParams.set('page', '1');
if (searchTransporterKey) url.searchParams.set('transporter_key', searchTransporterKey);
else url.searchParams.delete('transporter_key');
if (searchDriverName) url.searchParams.set('driver_name', searchDriverName);
@@ -74,39 +94,21 @@
const columns = createColumns(loadData);
</script>
<div class="flex h-full flex-col space-y-6 p-8">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Conductores</h2>
<h1 class="text-2xl font-bold tracking-tight">Conductores</h1>
<p class="text-muted-foreground">Gestión del catálogo de conductores</p>
</div>
<Button onclick={() => (createDialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" /> Nuevo Conductor
</Button>
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={loadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Conductor</Button></div>
</div>
<div class="flex items-center space-x-2">
<Input
placeholder="Buscar por clave transportista..."
class="h-8 w-[250px] bg-card"
bind:value={searchTransporterKey}
oninput={handleSearch}
/>
<Input
placeholder="Buscar por nombre..."
class="h-8 w-[250px] bg-card"
bind:value={searchDriverName}
oninput={handleSearch}
/>
</div>
<Card.Root class="border bg-background flex flex-col">
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Conductores</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Clave transportista" class="h-9 w-44 bg-card lg:w-56" bind:value={searchTransporterKey} oninput={handleSearch} /><Input placeholder="Nombre" class="h-9 w-44 bg-card lg:w-56" bind:value={searchDriverName} oninput={handleSearch} /></div></div></Card.Header>
<Card.Content class="p-0">{#if loading && data.length === 0}<div class="flex h-64 items-center justify-center text-muted-foreground">Cargando conductores...</div>{:else}<div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable {data} {columns} {loading} {hasMore} {loadMore} /></div>{/if}</Card.Content>
</Card.Root>
{#if loading && data.length === 0}
<div class="flex h-64 items-center justify-center text-muted-foreground">
Cargando conductores...
</div>
{:else}
<DataTable {data} {columns} {pageCount} {totalItems} />
{/if}
<div class="flex-none text-sm text-muted-foreground">Mostrando {data.length} de {totalItems} registros</div>
<CreateEditDialog bind:open={createDialogOpen} onSuccess={loadData} />
</div>

View File

@@ -2,17 +2,21 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import DataTable from '$lib/components/dashboard/general_catalogs/electronic_notices/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/electronic_notices/columns.js';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/electronic_notices/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaAvisosElectrónicos } from '$lib/config/shortcuts/dashboard/general_catalogs/electronic_notices/list';
import { getElectronicNotices } from '$lib/api/dashboard/a76/general_catalogs/electronic-notices';
import { companyStore } from '$lib/stores/company.svelte';
let { data } = $props();
let dialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Atajos
useShortcuts(
@@ -28,10 +32,52 @@
let searchPedimento = $state($page.url.searchParams.get('pedimento') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
let allItems = $state(data.notices?.items || []);
let currentPage = $state(data.notices?.page || 1);
let pageSize = $state(data.notices?.page_size || 50);
let totalItems = $state(data.notices?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.notices) {
allItems = data.notices.items || [];
currentPage = data.notices.page || 1;
totalItems = data.notices.total || 0;
pageSize = data.notices.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchNotice) filters.notice_number = searchNotice;
if (searchPedimento) filters.pedimento = searchPedimento;
const response = await getElectronicNotices(
1,
pageSize,
filters,
companyStore.activeCompany.id
);
if (response?.items) {
allItems = response.items;
currentPage = 1;
totalItems = response.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchNotice) url.searchParams.set('notice_number', searchNotice);
else url.searchParams.delete('notice_number');
@@ -39,54 +85,92 @@
if (searchPedimento) url.searchParams.set('pedimento', searchPedimento);
else url.searchParams.delete('pedimento');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchNotice) filters.notice_number = searchNotice;
if (searchPedimento) filters.pedimento = searchPedimento;
const response = await getElectronicNotices(
currentPage + 1,
pageSize,
filters,
companyStore.activeCompany.id
);
if (response?.items) {
allItems = [...allItems, ...response.items];
currentPage += 1;
totalItems = response.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more electronic notices:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchNotice) filters.notice_number = searchNotice;
if (searchPedimento) filters.pedimento = searchPedimento;
const response = await getElectronicNotices(
1,
pageSize,
filters,
companyStore.activeCompany.id
);
if (response?.items) {
allItems = response.items;
currentPage = 1;
totalItems = response.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading electronic notices:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Avisos Electrónicos</h1>
<p class="text-muted-foreground">Gestión del catálogo de avisos electrónicos</p>
</div>
<Button onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Aviso
</Button>
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (dialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Aviso</Button></div>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por No. Aviso..."
bind:value={searchNotice}
oninput={handleSearch}
/>
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por Pedimento..."
bind:value={searchPedimento}
oninput={handleSearch}
/>
</div>
</div>
{/if}
<div class="rounded-md border">
<DataTable
data={data.notices?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.notices?.pages || 0}
totalItems={data.notices?.total || 0}
/>
</div>
<Card.Root class="border bg-background flex flex-col">
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Avisos</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="No. aviso" bind:value={searchNotice} oninput={handleSearch} class="h-9 w-40 bg-card lg:w-52" /><Input placeholder="Pedimento" bind:value={searchPedimento} oninput={handleSearch} class="h-9 w-40 bg-card lg:w-52" /></div></div></Card.Header>
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createColumns(handleSuccess)} {loading} {hasMore} {loadMore} /></div></Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
</div>

View File

@@ -4,16 +4,20 @@
import { browser } from '$app/environment';
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import { createCatalogColumns } from '$lib/components/dashboard/general_catalogs/equivalencies/catalog-columns';
import DataTable from '$lib/components/dashboard/general_catalogs/equivalencies/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaEquivalencias } from '$lib/config/shortcuts/dashboard/general_catalogs/equivalencies/list';
import DataEquivalenciesDialog from '$lib/components/dashboard/general_catalogs/equivalencies/data-equivalencies-dialog.svelte';
import type { PageData } from './$types';
import { getEquivalencies } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import { companyStore } from '$lib/stores/company.svelte';
let { data }: { data: PageData } = $props();
let error = $state<string | null>(data.error || null);
let dataDialogOpen = $state(false);
let selectedEquivalency = $state<Equivalency | null>(null);
@@ -36,22 +40,115 @@
let searchFrom = $state($page.url.searchParams.get('from_unit_code') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
let allItems = $state(data.equivalencies?.items || []);
let currentPage = $state(data.equivalencies?.page || 1);
let pageSize = $state(data.equivalencies?.page_size || 50);
let totalItems = $state(data.equivalencies?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.equivalencies) {
allItems = data.equivalencies.items || [];
currentPage = data.equivalencies.page || 1;
totalItems = data.equivalencies.total || 0;
pageSize = data.equivalencies.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchFrom) filters.from_unit_code = searchFrom;
const response = await getEquivalencies(
1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchFrom) url.searchParams.set('from_unit_code', searchFrom);
else url.searchParams.delete('from_unit_code');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchFrom) filters.from_unit_code = searchFrom;
const response = await getEquivalencies(
currentPage + 1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage += 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more equivalencies:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchFrom) filters.from_unit_code = searchFrom;
const response = await getEquivalencies(
1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading equivalencies:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
reloadData();
}
function handleOpenInsertItems(equivalency: Equivalency) {
@@ -67,13 +164,13 @@
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Equivalencias</h1>
<p class="text-muted-foreground">Catálogo de equivalencias</p>
</div>
<Button
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9"
onclick={() => {
selectedEquivalency = null;
dataMode = 'create';
@@ -82,29 +179,21 @@
>
<Plus class="mr-2 h-4 w-4" />
Nueva Equivalencia
</Button>
</Button></div>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por código origen..."
bind:value={searchFrom}
oninput={handleSearch}
/>
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
</div>
{/if}
<DataTable
data={data.equivalencies.items}
columns={createCatalogColumns({
onInsertItems: handleOpenInsertItems,
onEdit: handleOpenEditCatalog,
onSuccess: handleSuccess
})}
pageCount={data.equivalencies.pages}
totalItems={data.equivalencies.total}
/>
<Card.Root class="border bg-background flex flex-col">
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Equivalencias</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Código origen" bind:value={searchFrom} oninput={handleSearch} class="h-9 w-40 bg-card lg:w-52" /></div></div></Card.Header>
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createCatalogColumns({ onInsertItems: handleOpenInsertItems, onEdit: handleOpenEditCatalog, onSuccess: handleSuccess })} {loading} {hasMore} {loadMore} /></div></Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
<DataEquivalenciesDialog
bind:open={dataDialogOpen}

View File

@@ -3,17 +3,21 @@
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { createColumns } from '$lib/components/dashboard/general_catalogs/error_catalogs/columns';
import DataTable from '$lib/components/dashboard/general_catalogs/error_catalogs/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import CreateDialog from '$lib/components/dashboard/general_catalogs/error_catalogs/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaErrores } from '$lib/config/shortcuts/dashboard/general_catalogs/error_catalogs/list';
import type { PageData } from './$types';
import { getErrorCatalogs } from '$lib/api/dashboard/a76/general_catalogs/error-catalogs';
import { companyStore } from '$lib/stores/company.svelte';
let { data }: { data: PageData } = $props();
let dialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Atajos
useShortcuts(
@@ -29,10 +33,52 @@
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
let allItems = $state(data.errors?.items || []);
let currentPage = $state(data.errors?.page || 1);
let pageSize = $state(data.errors?.page_size || 50);
let totalItems = $state(data.errors?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.errors) {
allItems = data.errors.items || [];
currentPage = data.errors.page || 1;
totalItems = data.errors.total || 0;
pageSize = data.errors.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getErrorCatalogs(
companyStore.activeCompany.id,
1,
pageSize,
filters
);
if (response?.items) {
allItems = response.items;
currentPage = 1;
totalItems = response.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
@@ -40,50 +86,92 @@
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getErrorCatalogs(
companyStore.activeCompany.id,
currentPage + 1,
pageSize,
filters
);
if (response?.items) {
allItems = [...allItems, ...response.items];
currentPage += 1;
totalItems = response.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more error catalogs:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getErrorCatalogs(
companyStore.activeCompany.id,
1,
pageSize,
filters
);
if (response?.items) {
allItems = response.items;
currentPage = 1;
totalItems = response.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading error catalogs:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Errores de Facturación</h1>
<p class="text-muted-foreground">Gestión del catálogo de errores de facturación</p>
</div>
<Button onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Error
</Button>
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (dialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Error</Button></div>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
{/if}
<div class="rounded-md border">
<DataTable
data={data.errors?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.errors?.pages || 0}
totalItems={data.errors?.total || 0}
/>
</div>
<Card.Root class="border bg-background flex flex-col">
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Errores</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Código" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /><Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createColumns(handleSuccess)} {loading} {hasMore} {loadMore} /></div></Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
<CreateDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
</div>

View File

@@ -4,17 +4,21 @@
import { browser } from '$app/environment';
import { createColumns } from '$lib/components/dashboard/exchange_rate/columns';
import CreateEditDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/exchange_rate/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaTiposCambio } from '$lib/config/shortcuts/dashboard/general_catalogs/exchange_rate/list';
import type { PageData } from './$types';
import { getExchangeRates } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
import { companyStore } from '$lib/stores/company.svelte';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Atajos
useShortcuts(
@@ -36,55 +40,127 @@
let searchCurrency = $state($page.url.searchParams.get('local_currency') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
let allItems = $state(data.exchange_rates?.items || []);
let currentPage = $state(data.exchange_rates?.page || 1);
let pageSize = $state(data.exchange_rates?.page_size || 50);
let totalItems = $state(data.exchange_rates?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.exchange_rates) {
allItems = data.exchange_rates.items || [];
currentPage = data.exchange_rates.page || 1;
totalItems = data.exchange_rates.total || 0;
pageSize = data.exchange_rates.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const response = await getExchangeRates(companyStore.activeCompany.id, {
local_currency: searchCurrency || undefined,
page: 1,
page_size: pageSize
});
if (response.data) {
allItems = response.data.items;
currentPage = response.data.page || 1;
totalItems = response.data.total || 0;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchCurrency) url.searchParams.set('local_currency', searchCurrency);
else url.searchParams.delete('local_currency');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const response = await getExchangeRates(companyStore.activeCompany.id, {
local_currency: searchCurrency || undefined,
page: currentPage + 1,
page_size: pageSize
});
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage += 1;
totalItems = response.data.total || 0;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more exchange rates:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const response = await getExchangeRates(companyStore.activeCompany.id, {
local_currency: searchCurrency || undefined,
page: 1,
page_size: pageSize
});
if (response.data) {
allItems = response.data.items;
currentPage = response.data.page || 1;
totalItems = response.data.total || 0;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading exchange rates:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Tipos de Cambio</h1>
<p class="text-muted-foreground">Catálogo de tipos de cambio</p>
</div>
<Button onclick={() => (createDialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Tipo de Cambio
</Button>
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Tipo de Cambio</Button></div>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por moneda..."
bind:value={searchCurrency}
oninput={handleSearch}
/>
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
</div>
{/if}
<div class="rounded-md border">
<DataTable
data={data.exchange_rates?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.exchange_rates?.pages || 0}
totalItems={data.exchange_rates?.total || 0}
/>
</div>
<Card.Root class="border bg-background flex flex-col">
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Tipos de Cambio</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Moneda" bind:value={searchCurrency} oninput={handleSearch} class="h-9 w-40 bg-card lg:w-52" /></div></div></Card.Header>
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createColumns(handleSuccess)} {loading} {hasMore} {loadMore} /></div></Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
</div>

View File

@@ -4,17 +4,21 @@
import { browser } from '$app/environment';
import { createColumns } from '$lib/components/dashboard/general_catalogs/identifiers/columns';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/identifiers/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/identifiers/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaIdentificadores } from '$lib/config/shortcuts/dashboard/general_catalogs/identifiers/list';
import { getIdentifiers } from '$lib/api/dashboard/a76/general_catalogs/identifiers';
import { companyStore } from '$lib/stores/company.svelte';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Atajos
useShortcuts(
@@ -30,10 +34,47 @@
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
let allItems = $state(data.identifiers?.items || []);
let currentPage = $state(data.identifiers?.page || 1);
let pageSize = $state(data.identifiers?.page_size || 50);
let totalItems = $state(data.identifiers?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.identifiers) {
allItems = data.identifiers.items || [];
currentPage = data.identifiers.page || 1;
totalItems = data.identifiers.total || 0;
pageSize = data.identifiers.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getIdentifiers(1, pageSize, companyStore.activeCompany.id, filters);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
@@ -41,49 +82,125 @@
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getIdentifiers(
currentPage + 1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage += 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more identifiers:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getIdentifiers(1, pageSize, companyStore.activeCompany.id, filters);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading identifiers:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Identificadores</h1>
<p class="text-muted-foreground">Catálogo de identificadores</p>
</div>
<Button onclick={() => (createDialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Identificador
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por clave..." bind:value={searchCode} oninput={handleSearch} />
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar
</Button>
<Button class="h-9" onclick={() => (createDialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Identificador
</Button>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.identifiers.items}
columns={createColumns(handleSuccess)}
pageCount={data.identifiers.pages}
totalItems={data.identifiers.total}
/>
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<Card.Root class="border bg-background flex flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Identificadores</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<Input
placeholder="Clave"
bind:value={searchCode}
oninput={handleSearch}
class="h-9 w-36 bg-card lg:w-44"
/>
<Input
placeholder="Descripción"
bind:value={searchDesc}
oninput={handleSearch}
class="h-9 w-44 bg-card lg:w-64"
/>
</div>
</div>
</Card.Header>
<Card.Content class="p-0">
<div class="rounded-md border bg-background overflow-hidden">
<InfiniteDataTable
data={allItems}
columns={createColumns(handleSuccess)}
{loading}
{hasMore}
{loadMore}
/>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">
Mostrando {allItems.length} de {totalItems} registros
</div>
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />

View File

@@ -4,17 +4,21 @@
import { browser } from '$app/environment';
import { createColumns } from '$lib/components/dashboard/general_catalogs/inpc/columns';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/inpc/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/inpc/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaInpc } from '$lib/config/shortcuts/dashboard/general_catalogs/inpc/list';
import { getINPCs } from '$lib/api/dashboard/a76/general_catalogs/inpc';
import { companyStore } from '$lib/stores/company.svelte';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Atajos
useShortcuts(
@@ -30,10 +34,48 @@
let searchMonth = $state($page.url.searchParams.get('month') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
// Infinite scroll
let allItems = $state(data.inpc?.items || []);
let currentPage = $state(data.inpc?.page || 1);
let pageSize = $state(data.inpc?.page_size || 50);
let totalItems = $state(data.inpc?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.inpc) {
allItems = data.inpc.items || [];
currentPage = data.inpc.page || 1;
totalItems = data.inpc.total || 0;
pageSize = data.inpc.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchYear) filters.year = searchYear;
if (searchMonth) filters.month = searchMonth;
const response = await getINPCs(1, pageSize, companyStore.activeCompany.id, filters);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchYear) url.searchParams.set('year', searchYear);
else url.searchParams.delete('year');
@@ -41,45 +83,125 @@
if (searchMonth) url.searchParams.set('month', searchMonth);
else url.searchParams.delete('month');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchYear) filters.year = searchYear;
if (searchMonth) filters.month = searchMonth;
const response = await getINPCs(
currentPage + 1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage += 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more INPC:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchYear) filters.year = searchYear;
if (searchMonth) filters.month = searchMonth;
const response = await getINPCs(1, pageSize, companyStore.activeCompany.id, filters);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading INPC:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex-none flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">INPC</h1>
<p class="text-muted-foreground">Índice Nacional de Precios al Consumidor</p>
</div>
<Button onclick={() => (createDialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo INPC
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por año..." bind:value={searchYear} oninput={handleSearch} />
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por mes..." bind:value={searchMonth} oninput={handleSearch} />
<div class="flex items-center gap-3">
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar
</Button>
<Button onclick={() => (createDialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo INPC
</Button>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.inpc.items}
columns={createColumns(handleSuccess)}
pageCount={data.inpc.pages}
totalItems={data.inpc.total}
/>
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<Card.Root class="border bg-background flex flex-1 min-h-0 flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de INPC</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<Input
placeholder="Año"
bind:value={searchYear}
oninput={handleSearch}
class="h-9 w-28 bg-card lg:w-36"
/>
<Input
placeholder="Mes"
bind:value={searchMonth}
oninput={handleSearch}
class="h-9 w-28 bg-card lg:w-36"
/>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 flex-1 p-0">
<div class="flex h-full min-h-0 rounded-md border bg-background overflow-hidden flex-col">
<InfiniteDataTable
data={allItems}
columns={createColumns(handleSuccess)}
{loading}
{hasMore}
{loadMore}
/>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">
Mostrando {allItems.length} de {totalItems} registros
</div>
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />

View File

@@ -1,18 +1,22 @@
<script lang="ts">
import { page } from '$app/stores';
import DataTable from '$lib/components/dashboard/general_catalogs/legends/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { createColumns } from '$lib/components/dashboard/general_catalogs/legends/columns';
import CreateDialog from '$lib/components/dashboard/general_catalogs/legends/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaLeyendas } from '$lib/config/shortcuts/dashboard/general_catalogs/legends/list';
import { getLegends } from '$lib/api/dashboard/a76/general_catalogs/legends';
import { companyStore } from '$lib/stores/company.svelte';
let { data } = $props();
let dialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Atajos
useShortcuts(
@@ -28,17 +32,49 @@
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
const columns = $derived(createColumns(handleSuccess));
function handleSearch() {
let allItems = $state(data.legends?.items || []);
let currentPage = $state(data.legends?.page || 1);
let pageSize = $state(data.legends?.page_size || 50);
let totalItems = $state(data.legends?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.legends) {
allItems = data.legends.items || [];
currentPage = data.legends.page || 1;
totalItems = data.legends.total || 0;
pageSize = data.legends.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getLegends(1, pageSize, companyStore.activeCompany.id, filters);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
@@ -46,44 +82,125 @@
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getLegends(
currentPage + 1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage += 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more legends:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getLegends(1, pageSize, companyStore.activeCompany.id, filters);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading legends:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex-none flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Leyendas Fijas</h1>
<p class="text-muted-foreground">Gestión del catálogo de leyendas fijas</p>
</div>
<Button onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nueva Leyenda
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar
</Button>
<Button class="h-9" onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nueva Leyenda
</Button>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.legends?.items || []}
{columns}
pageCount={data.legends?.pages || 0}
totalItems={data.legends?.total || 0}
/>
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<Card.Root class="border bg-background flex flex-1 min-h-0 flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Leyendas</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<Input
placeholder="Código"
bind:value={searchCode}
oninput={handleSearch}
class="h-9 w-36 bg-card lg:w-44"
/>
<Input
placeholder="Descripción"
bind:value={searchDesc}
oninput={handleSearch}
class="h-9 w-44 bg-card lg:w-64"
/>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 flex-1 p-0">
<div class="flex h-full min-h-0 rounded-md border bg-background overflow-hidden flex-col">
<InfiniteDataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">
Mostrando {allItems.length} de {totalItems} registros
</div>
<CreateDialog bind:open={dialogOpen} onSuccess={handleSuccess} />

View File

@@ -1,17 +1,25 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import type { ColumnDef } from '@tanstack/table-core';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import CreateDialog from '$lib/components/dashboard/general_catalogs/multi_currency_types/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaMonedas } from '$lib/config/shortcuts/dashboard/general_catalogs/multi_currency_types/list';
import {
getMultiCurrencyTypes,
type MultiCurrencyType
} from '$lib/api/dashboard/a76/general_catalogs/multi-currency-types';
import { companyStore } from '$lib/stores/company.svelte';
let { data } = $props();
let dialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Atajos
useShortcuts(
@@ -27,22 +35,74 @@
let searchCountry = $state($page.url.searchParams.get('country_key') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'currency_type_code', label: 'Código Moneda' },
{ key: 'country_key', label: 'País' },
{ key: 'conversion_factor', label: 'Factor Conversión' },
{ key: 'publication_date', label: 'Fecha Publicación' }
const columns: ColumnDef<MultiCurrencyType>[] = [
{
accessorKey: 'currency_type_code',
header: 'Código Moneda',
cell: ({ row }) => row.original.currency_type_code || '-'
},
{
accessorKey: 'country_key',
header: 'País',
cell: ({ row }) => row.original.country_key || '-'
},
{
accessorKey: 'conversion_factor',
header: 'Factor Conversión',
cell: ({ row }) => row.original.conversion_factor ?? '-'
},
{
accessorKey: 'publication_date',
header: 'Fecha Publicación',
cell: ({ row }) => row.original.publication_date || '-'
}
];
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
let allItems = $state(data.types?.items || []);
let currentPage = $state(data.types?.page || 1);
let pageSize = $state(data.types?.page_size || 50);
let totalItems = $state(data.types?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
function handleSearch() {
$effect(() => {
if (data.types) {
allItems = data.types.items || [];
currentPage = data.types.page || 1;
totalItems = data.types.total || 0;
pageSize = data.types.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const response = await getMultiCurrencyTypes(
companyStore.activeCompany.id,
1,
pageSize,
{
currency_type_code: searchCode || undefined,
country_key: searchCountry || undefined
}
);
if (response.data?.items) {
allItems = response.data.items;
currentPage = response.data.page || 1;
totalItems = response.data.total || 0;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchCode) url.searchParams.set('currency_type_code', searchCode);
else url.searchParams.delete('currency_type_code');
@@ -50,41 +110,90 @@
if (searchCountry) url.searchParams.set('country_key', searchCountry);
else url.searchParams.delete('country_key');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const response = await getMultiCurrencyTypes(
companyStore.activeCompany.id,
currentPage + 1,
pageSize,
{
currency_type_code: searchCode || undefined,
country_key: searchCountry || undefined
}
);
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage += 1;
totalItems = response.data.total || 0;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more multi-currency types:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const response = await getMultiCurrencyTypes(
companyStore.activeCompany.id,
1,
pageSize,
{
currency_type_code: searchCode || undefined,
country_key: searchCountry || undefined
}
);
if (response.data?.items) {
allItems = response.data.items;
currentPage = response.data.page || 1;
totalItems = response.data.total || 0;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading multi-currency types:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Tipos de Moneda Múltiple</h1>
<p class="text-muted-foreground">Gestión del catálogo de tipos de moneda múltiple</p>
</div>
<Button onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Tipo
</Button>
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (dialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Tipo</Button></div>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por país..." bind:value={searchCountry} oninput={handleSearch} />
</div>
</div>
{/if}
<div class="rounded-md border">
<SimpleDataTable
data={data.types?.items || []}
{columns}
pageCount={data.types?.pages || 0}
totalItems={data.types?.total || 0}
/>
</div>
<Card.Root class="border bg-background flex flex-col">
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Monedas</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Código" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /><Input placeholder="País" bind:value={searchCountry} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /></div></div></Card.Header>
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
<CreateDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
</div>

View File

@@ -1,21 +1,25 @@
<script lang="ts">
import { page } from '$app/stores';
import DataTable from '$lib/components/dashboard/packages/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import { createColumns } from '$lib/components/dashboard/packages/columns';
import CreateEditDialog from '$lib/components/dashboard/packages/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import * as m from '$lib/paraglide/messages.js';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaBultos } from '$lib/config/shortcuts/dashboard/general_catalogs/packages/list';
import { getPackages } from '$lib/api/dashboard/a76/general_catalogs/packages';
import { companyStore } from '$lib/stores/company.svelte';
let { data } = $props();
let createDialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Filtros
let searchCode = $state($page.url.searchParams.get('code') || '');
@@ -31,62 +35,173 @@
})
);
function handleSearch() {
let allItems = $state(data.packages?.items || []);
let currentPage = $state(data.packages?.page || 1);
let pageSize = $state(data.packages?.page_size || 50);
let totalItems = $state(data.packages?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.packages) {
allItems = data.packages.items || [];
currentPage = data.packages.page || 1;
totalItems = data.packages.total || 0;
pageSize = data.packages.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchName) filters.name = searchName;
const response = await getPackages(1, pageSize, companyStore.activeCompany.id, filters);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
if (searchName) url.searchParams.set('name', searchName);
else url.searchParams.delete('name');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchName) filters.name = searchName;
const response = await getPackages(
currentPage + 1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage += 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more packages:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchName) filters.name = searchName;
const response = await getPackages(1, pageSize, companyStore.activeCompany.id, filters);
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading packages:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
// Recargar datos
const url = new URL($page.url);
goto(url, { invalidateAll: true });
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Bultos y Embalajes</h1>
<p class="text-muted-foreground">Gestión del catálogo de bultos y embalajes</p>
</div>
<Button onclick={() => (createDialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Bulto
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por clave..." bind:value={searchCode} oninput={handleSearch} />
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchName}
oninput={handleSearch}
/>
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar
</Button>
<Button class="h-9" onclick={() => (createDialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Bulto
</Button>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.packages?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.packages?.pages || 0}
totalItems={data.packages?.total || 0}
/>
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<Card.Root class="border bg-background flex flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Bultos</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<Input
placeholder="Clave"
bind:value={searchCode}
oninput={handleSearch}
class="h-9 w-36 bg-card lg:w-44"
/>
<Input
placeholder="Descripción"
bind:value={searchName}
oninput={handleSearch}
class="h-9 w-44 bg-card lg:w-64"
/>
</div>
</div>
</Card.Header>
<Card.Content class="p-0">
<div class="rounded-md border bg-background overflow-hidden">
<InfiniteDataTable
data={allItems}
columns={createColumns(handleSuccess)}
{loading}
{hasMore}
{loadMore}
/>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">
Mostrando {allItems.length} de {totalItems} registros
</div>
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />

View File

@@ -4,17 +4,21 @@
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/general_catalogs/units_of_measure/ace/data-table.svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaPuertos } from '$lib/config/shortcuts/dashboard/general_catalogs/ports/list';
import { portsApi } from '$lib/api/dashboard/a76/general_catalogs/ports';
import { companyStore } from '$lib/stores/company.svelte';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let error = $state<string | null>(data.items?.error || null);
// Atajos
useShortcuts(
@@ -30,10 +34,50 @@
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
let allItems = $state(data.items?.items || data.items || []);
let currentPage = $state(data.items?.page || 1);
let pageSize = $state(data.items?.pageSize || data.items?.page_size || 50);
let totalItems = $state(data.items?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
$effect(() => {
if (data.items?.items) {
allItems = data.items.items || [];
currentPage = data.items.page || 1;
totalItems = data.items.total || 0;
pageSize = data.items.pageSize || data.items.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const params: Record<string, string> = {
page: '1',
page_size: pageSize.toString()
};
if (searchCode) params.port_code = searchCode;
if (searchDesc) params.description = searchDesc;
const response = await portsApi.list(companyStore.activeCompany.id, params);
if (response.data?.items) {
allItems = response.data.items;
currentPage = response.data.page || 1;
totalItems = response.data.total || 0;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchCode) url.searchParams.set('port_code', searchCode);
else url.searchParams.delete('port_code');
@@ -41,58 +85,117 @@
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const params: Record<string, string> = {
page: (currentPage + 1).toString(),
page_size: pageSize.toString()
};
if (searchCode) params.port_code = searchCode;
if (searchDesc) params.description = searchDesc;
const response = await portsApi.list(companyStore.activeCompany.id, params);
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage += 1;
totalItems = response.data.total || 0;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more ports:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const params: Record<string, string> = {
page: '1',
page_size: pageSize.toString()
};
if (searchCode) params.port_code = searchCode;
if (searchDesc) params.description = searchDesc;
const response = await portsApi.list(companyStore.activeCompany.id, params);
if (response.data?.items) {
allItems = response.data.items;
currentPage = response.data.page || 1;
totalItems = response.data.total || 0;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading ports:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Puertos</h1>
<p class="text-muted-foreground">Catálogo de puertos</p>
</div>
<Button onclick={() => (createDialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Puerto
</Button>
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar
</Button>
<Button class="h-9" onclick={() => (createDialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Puerto
</Button>
</div>
</div>
{#if data.items?.error}
{#if error}
<div
class="rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"
>
{data.items.error}
{error}
</div>
{/if}
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<Card.Root class="border bg-background flex flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Puertos</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<Input placeholder="Código" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" />
<Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
</div>
</div>
</Card.Header>
<Card.Content class="p-0">
<div class="rounded-md border bg-background overflow-hidden">
<InfiniteDataTable
data={allItems}
columns={createColumns(handleSuccess)}
{loading}
{hasMore}
{loadMore}
/>
</div>
</Card.Content>
</Card.Root>
<div class="rounded-md border">
<DataTable
data={data.items?.items || data.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.items?.pages || data.items?.pageCount || 0}
totalItems={data.items?.total || 0}
/>
</div>
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
</div>

View File

@@ -2,17 +2,21 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/prevalidators/data-table.svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/prevalidators/columns';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/prevalidators/create-edit-dialog.svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaPrevalidadores } from '$lib/config/shortcuts/dashboard/general_catalogs/prevalidators/list';
import { getPrevalidators } from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
import { companyStore } from '$lib/stores/company.svelte';
let { data } = $props();
let dialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Atajos
useShortcuts(
@@ -27,15 +31,47 @@
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
let allItems = $state(data.prevalidators?.items || []);
let currentPage = $state(data.prevalidators?.page || 1);
let pageSize = $state(data.prevalidators?.page_size || 50);
let totalItems = $state(data.prevalidators?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
function handleSearch() {
$effect(() => {
if (data.prevalidators) {
allItems = data.prevalidators.items || [];
currentPage = data.prevalidators.page || 1;
totalItems = data.prevalidators.total || 0;
pageSize = data.prevalidators.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getPrevalidators(1, pageSize, filters, companyStore.activeCompany.id);
if (response?.items) {
allItems = response.items;
currentPage = 1;
totalItems = response.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
@@ -43,45 +79,98 @@
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getPrevalidators(
currentPage + 1,
pageSize,
filters,
companyStore.activeCompany.id
);
if (response?.items) {
allItems = [...allItems, ...response.items];
currentPage += 1;
totalItems = response.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more prevalidators:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
if (searchDesc) filters.description = searchDesc;
const response = await getPrevalidators(1, pageSize, filters, companyStore.activeCompany.id);
if (response?.items) {
allItems = response.items;
currentPage = 1;
totalItems = response.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading prevalidators:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Prevalidadores</h1>
<p class="text-muted-foreground">Catálogo de prevalidadores</p>
</div>
<Button onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Prevalidador
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button>
<Button class="h-9" onclick={() => (dialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Prevalidador</Button>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.prevalidators?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.prevalidators?.pages || 0}
totalItems={data.prevalidators?.total || 0}
/>
</div>
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<Card.Root class="border bg-background flex flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Prevalidadores</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<Input placeholder="Código" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" />
<Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
</div>
</div>
</Card.Header>
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createColumns(handleSuccess)} {loading} {hasMore} {loadMore} /></div></Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
</div>

View File

@@ -1,8 +1,9 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { Plus, RefreshCw } from 'lucide-svelte';
import DataTable from '$lib/components/dashboard/seal/data-table.svelte';
import CreateEditDialog from '$lib/components/dashboard/seal/create-edit-dialog.svelte';
import { createColumns } from '$lib/components/dashboard/seal/columns';
@@ -17,6 +18,7 @@
let error = $state<string | null>(null);
let currentPage = $state(1);
const pageSize = 50;
let totalItems = $state(0);
let dialogOpen = $state(false);
@@ -104,6 +106,7 @@
allItems = [...allItems, ...response.data.items];
}
totalItems = response.data.total ?? allItems.length;
currentPage = page;
} catch (err: any) {
error = err.message || 'Error al cargar los sellos';
@@ -130,40 +133,61 @@
loadInitialData();
}
function reloadData() {
loadInitialData();
}
const columns = createColumns(handleSuccess);
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex-none flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Sellos</h1>
<p class="text-muted-foreground">Gestiona los sellos de tu empresa</p>
</div>
<Button onclick={handleCreate}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Sello
</Button>
<div class="flex items-center gap-3">
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar
</Button>
<Button class="h-9" onclick={handleCreate}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Sello
</Button>
</div>
</div>
{#if error}
<div class="rounded-md bg-destructive/15 p-4">
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
<p class="text-sm text-destructive">{error}</p>
</div>
{/if}
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Filtrar por sello..."
bind:value={sealFilter}
oninput={handleSearch}
disabled={loading}
/>
</div>
</div>
<Card.Root class="border bg-background flex flex-1 min-h-0 flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Sellos</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<Input
placeholder="Filtrar por sello"
bind:value={sealFilter}
oninput={handleSearch}
disabled={loading}
class="h-9 w-44 bg-card lg:w-64"
/>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 flex-1 p-0">
<div class="flex h-full min-h-0 rounded-md border bg-background overflow-hidden flex-col">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</Card.Content>
</Card.Root>
<div class="rounded-md border">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
<div class="flex-none text-sm text-muted-foreground">
Mostrando {allItems.length} de {totalItems || allItems.length} registros
</div>
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />

View File

@@ -4,15 +4,19 @@
import { browser } from '$app/environment';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/signatures/data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { Plus, RefreshCw } from 'lucide-svelte';
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/signatures/columns';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/signatures/create-edit-dialog.svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaFirmas } from '$lib/config/shortcuts/dashboard/general_catalogs/signatures/list';
import { getSignatures } from '$lib/api/dashboard/a76/general_catalogs/signatures';
import { companyStore } from '$lib/stores/company.svelte';
let { data } = $props();
let dialogOpen = $state(false);
let error = $state<string | null>(data.error || null);
// Atajos
useShortcuts(
@@ -26,50 +30,138 @@
let searchCode = $state($page.url.searchParams.get('code') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
let allItems = $state(data.signatures?.items || []);
let currentPage = $state(data.signatures?.page || 1);
let pageSize = $state(data.signatures?.page_size || 50);
let totalItems = $state(data.signatures?.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
function handleSearch() {
$effect(() => {
if (data.signatures) {
allItems = data.signatures.items || [];
currentPage = data.signatures.page || 1;
totalItems = data.signatures.total || 0;
pageSize = data.signatures.page_size || pageSize;
}
});
async function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
const response = await getSignatures(
1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response?.items) {
allItems = response.items;
currentPage = 1;
totalItems = response.total;
}
} catch (err) {
error = 'Error aplicando filtros';
console.error('Error applying filters:', err);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
const response = await getSignatures(
currentPage + 1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response?.items) {
allItems = [...allItems, ...response.items];
currentPage += 1;
totalItems = response.total;
}
} catch (err) {
error = 'Error cargando mas datos';
console.error('Error loading more signatures:', err);
} finally {
loading = false;
}
}
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: Record<string, string> = {};
if (searchCode) filters.code = searchCode;
const response = await getSignatures(
1,
pageSize,
companyStore.activeCompany.id,
filters
);
if (response?.items) {
allItems = response.items;
currentPage = 1;
totalItems = response.total;
}
} catch (err) {
error = 'Error al recargar datos';
console.error('Error reloading signatures:', err);
} finally {
loading = false;
}
}
function handleSuccess() {
reloadData();
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Firmas Electrónicas</h1>
<p class="text-muted-foreground">Gestión del catálogo de firmas electrónicas</p>
</div>
<Button onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nueva Firma
</Button>
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (dialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nueva Firma</Button></div>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
</div>
{/if}
<div class="rounded-md border">
<DataTable
data={data.signatures?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.signatures?.pages || 0}
totalItems={data.signatures?.total || 0}
/>
</div>
<Card.Root class="border bg-background flex flex-col">
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Firmas</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Código" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /></div></div></Card.Header>
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createColumns(handleSuccess)} {loading} {hasMore} {loadMore} /></div></Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
</div>

View File

@@ -89,145 +89,100 @@
}
</script>
<div class="container mx-auto p-6">
<Card.Root>
<Card.Header class="border-b text-center">
<Card.Title class="text-2xl font-bold uppercase">
Catálogo de Fracciones SITAR - SCAII
</Card.Title>
<p class="mt-2 text-muted-foreground">Nomenclatura arancelaria mexicana completa</p>
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-2xl font-bold tracking-tight">Catálogo de Fracciones SITAR - SCAII</h1>
<p class="text-muted-foreground">Nomenclatura arancelaria mexicana completa</p>
</div>
<Button variant="outline" size="sm" class="h-9" title="Exportar a CSV">
<Download class="mr-2 h-4 w-4" />
Exportar
</Button>
</div>
<Card.Root class="flex min-h-0 flex-1 flex-col border bg-background">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Fracciones</Card.Title>
<div class="relative min-w-[320px] flex-1 xl:max-w-xl">
<Search class="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
bind:value={searchQuery}
placeholder="Buscar por código, fracción, descripción, NICO o UMT..."
class="h-9 bg-card pl-9"
/>
</div>
</div>
</Card.Header>
<Card.Content class="pt-6">
<div class="space-y-6">
<!-- Barra de búsqueda y acciones -->
<div class="flex items-center gap-4">
<div class="relative flex-1">
<Search
class="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground"
/>
<Input
bind:value={searchQuery}
placeholder="Buscar por código, fracción, descripción, NICO o UMT (Búsqueda en servidor)..."
class="pl-9"
/>
</div>
<Button variant="outline" size="icon" title="Exportar a CSV">
<Download class="h-4 w-4" />
</Button>
</div>
<!-- Información de registros -->
<div class="flex items-center justify-between text-sm text-muted-foreground">
<div>
{#if isLoading}
<div class="flex items-center gap-2">
<Loader2 class="h-4 w-4 animate-spin" />
<span>Cargando...</span>
</div>
{:else}
Mostrando {tariffFractions.length} de {totalRecords} fracciones arancelarias
{#if searchQuery}
(filtrado)
{/if}
{/if}
</div>
{#if totalPages > 1}
<div class="flex items-center gap-2">
<span>Página {currentPage} de {totalPages}</span>
</div>
{/if}
</div>
<!-- Tabla de fracciones -->
<div class="max-h-[600px] overflow-auto rounded-md border">
<Table.Root>
<Table.Header class="z-10 bg-background">
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell bg-background">
<Table.Root>
<Table.Header class="catalog-table-header">
<Table.Row>
<Table.Head class="catalog-table-head-cell w-[100px]">Código</Table.Head>
<Table.Head class="catalog-table-head-cell w-[120px]">Fracción</Table.Head>
<Table.Head class="catalog-table-head-cell min-w-[350px]">Descripción</Table.Head>
<Table.Head class="catalog-table-head-cell w-[80px]">NICO</Table.Head>
<Table.Head class="catalog-table-head-cell w-[80px]">UMT</Table.Head>
<Table.Head class="catalog-table-head-cell w-[100px]">Adv. Impo</Table.Head>
<Table.Head class="catalog-table-head-cell w-[100px]">Adv. Expo</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if tariffFractions.length === 0}
<Table.Row>
<Table.Head class="w-[100px]">Código</Table.Head>
<Table.Head class="w-[120px]">Fracción</Table.Head>
<Table.Head class="min-w-[350px]">Descripción</Table.Head>
<Table.Head class="w-[80px]">NICO</Table.Head>
<Table.Head class="w-[80px]">UMT</Table.Head>
<Table.Head class="w-[100px]">Adv. Impo</Table.Head>
<Table.Head class="w-[100px]">Adv. Expo</Table.Head>
<Table.Cell colspan={7} class="h-24 text-center text-sm text-muted-foreground">
{#if isLoading}
Cargando fracciones arancelarias...
{:else if searchQuery}
No se encontraron fracciones que coincidan con la búsqueda
{:else}
No hay fracciones arancelarias disponibles
{/if}
</Table.Cell>
</Table.Row>
</Table.Header>
<Table.Body>
{#if tariffFractions.length === 0}
<Table.Row>
<Table.Cell colspan={7} class="py-8 text-center text-muted-foreground">
{#if isLoading}
Cargando fracciones arancelarias...
{:else if searchQuery}
No se encontraron fracciones que coincidan con la búsqueda
{:else}
No hay fracciones arancelarias disponibles
{/if}
</Table.Cell>
{:else}
{#each tariffFractions as fraction (fraction.id)}
<Table.Row class="catalog-table-row">
<Table.Cell class="font-mono text-sm">{fraction.code}</Table.Cell>
<Table.Cell class="font-mono text-sm font-medium">{fraction.fraction}</Table.Cell>
<Table.Cell class="text-sm">{fraction.description || '-'}</Table.Cell>
<Table.Cell class="text-sm">{fraction.nico || '-'}</Table.Cell>
<Table.Cell class="text-sm">{fraction.umt || '-'}</Table.Cell>
<Table.Cell class="text-sm">{fraction.adv_impo || '-'}</Table.Cell>
<Table.Cell class="text-sm">{fraction.adv_expo || '-'}</Table.Cell>
</Table.Row>
{:else}
{#each tariffFractions as fraction (fraction.id)}
<Table.Row class="hover:bg-muted/50">
<Table.Cell class="font-mono text-sm">{fraction.code}</Table.Cell>
<Table.Cell class="font-mono text-sm font-medium"
>{fraction.fraction}</Table.Cell
>
<Table.Cell class="text-sm">
{fraction.description || '-'}
</Table.Cell>
<Table.Cell class="text-sm">{fraction.nico || '-'}</Table.Cell>
<Table.Cell class="text-sm">{fraction.umt || '-'}</Table.Cell>
<Table.Cell class="text-sm">{fraction.adv_impo || '-'}</Table.Cell>
<Table.Cell class="text-sm">{fraction.adv_expo || '-'}</Table.Cell>
</Table.Row>
{/each}
{/if}
</Table.Body>
</Table.Root>
</div>
<!-- Paginación -->
{#if totalPages > 1}
<div class="flex items-center justify-center gap-2">
<Button
variant="outline"
size="sm"
disabled={currentPage === 1 || isLoading}
onclick={() => goToPage(1)}
>
Primera
</Button>
<Button
variant="outline"
size="sm"
disabled={currentPage === 1 || isLoading}
onclick={() => goToPage(currentPage - 1)}
>
Anterior
</Button>
<span class="px-4 text-sm">
Página {currentPage} de {totalPages}
</span>
<Button
variant="outline"
size="sm"
disabled={currentPage === totalPages || isLoading}
onclick={() => goToPage(currentPage + 1)}
>
Siguiente
</Button>
<Button
variant="outline"
size="sm"
disabled={currentPage === totalPages || isLoading}
onclick={() => goToPage(totalPages)}
>
Última
</Button>
</div>
{/if}
{/each}
{/if}
</Table.Body>
</Table.Root>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">
Mostrando {tariffFractions.length} de {totalRecords} fracciones arancelarias
{#if searchQuery}
<span class="ml-2"></span>
<span class="ml-2">Filtrado</span>
{/if}
</div>
{#if totalPages > 1}
<div class="flex-none flex items-center justify-center gap-2">
<Button variant="outline" size="sm" class="h-9" disabled={currentPage === 1 || isLoading} onclick={() => goToPage(1)}>
Primera
</Button>
<Button variant="outline" size="sm" class="h-9" disabled={currentPage === 1 || isLoading} onclick={() => goToPage(currentPage - 1)}>
Anterior
</Button>
<Button variant="outline" size="sm" class="h-9" disabled={currentPage === totalPages || isLoading} onclick={() => goToPage(currentPage + 1)}>
Siguiente
</Button>
<Button variant="outline" size="sm" class="h-9" disabled={currentPage === totalPages || isLoading} onclick={() => goToPage(totalPages)}>
Última
</Button>
</div>
{/if}
</div>

View File

@@ -3,9 +3,15 @@
import * as m from '$lib/paraglide/messages.js';
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold tracking-tight">{m['sidebar.fractions.canadian']()}</h1>
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-2xl font-bold tracking-tight">{m['sidebar.fractions.canadian']()}</h1>
<p class="text-muted-foreground">Catálogo de fracciones arancelarias canadienses</p>
</div>
</div>
<div class="flex-1 min-h-0 overflow-hidden flex flex-col">
<CanadianFractionList />
</div>
<CanadianFractionList />
</div>

Some files were not shown because too many files have changed in this diff Show More