feature/catalog-fixed-fix-styles

This commit is contained in:
2026-04-10 11:08:02 -06:00
parent 55821e69e5
commit 0b57742024
77 changed files with 2337 additions and 2666 deletions

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

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

@@ -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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +59,67 @@
});
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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,68 +60,67 @@
</script>
<div class="w-full">
<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">
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="relative 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

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

@@ -3,56 +3,38 @@
import { codePedimentoRegimensApi, type CodePedimentoRegimen } from '$lib/api/dashboard/reference_data/code_pedimento_regimens';
import DataTable from '$lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/code_pedimento_regimens/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/code_pedimento_regimens/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/code_pedimento_regimens/list';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Atajos
useShortcuts(
'Lista Códigos',
obtenerAtajosLista({
manejarNuevo: () => (showCreateDialog = true),
manejarActualizar: reloadData
})
);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -65,116 +47,118 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await codePedimentoRegimensApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await codePedimentoRegimensApi.list(currentPage + 1, pageSize);
try {
const response = await codePedimentoRegimensApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Crear columnas con el callback onSuccess
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Código Pedimento - Regímenes</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Pedimento - Regímenes
</h1>
<p class="text-muted-foreground">
Gestiona las relaciones entre códigos de pedimento y regímenes
Relación entre códigos de pedimento y regímenes aduaneros
</p>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Registro
</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>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por código de pedimento o régimen..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Relaciones</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</div>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,56 +3,29 @@
import { containersApi, type Container } from '$lib/api/dashboard/reference_data/containers';
import DataTable from '$lib/components/dashboard/reference_data/containers/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/containers/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/containers/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/containers/list';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Atajos
useShortcuts(
'Lista Contenedores',
obtenerAtajosLista({
manejarNuevo: () => (showCreateDialog = true),
manejarActualizar: reloadData
})
);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -65,116 +38,116 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await containersApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await containersApi.list(currentPage + 1, pageSize);
try {
const response = await containersApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Crear columnas con el callback onSuccess
useShortcuts('Contenedores', [
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Contenedores</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Contenedores
</h1>
<p class="text-muted-foreground">
Gestiona los tipos de contenedores disponibles
Gestiona los tipos de contenedores disponibles en el sistema
</p>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Contenedor
</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>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por clave o descripción de contenedor..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Contenedores</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,9 +3,10 @@
import { countriesApi, type Country } from '$lib/api/dashboard/reference_data/countries';
import DataTable from '$lib/components/dashboard/reference_data/countries/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/countries/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/countries/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import { Input } from '$lib/components/ui/input';
import { RefreshCw } from 'lucide-svelte';
import { page } from '$app/stores';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/countries/list';
@@ -14,14 +15,10 @@
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Atajos
useShortcuts(
'Lista Países',
obtenerAtajosLista({
manejarNuevo: () => (showCreateDialog = true),
manejarActualizar: reloadData
})
);
@@ -64,6 +61,37 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await countriesApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
// Actualizar URL silenciosamente para mantener estado
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
@@ -71,7 +99,7 @@
error = null;
try {
const response = await countriesApi.list(currentPage + 1, pageSize);
const response = await countriesApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
@@ -108,10 +136,6 @@
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
@@ -121,33 +145,46 @@
const columns = createColumns(handleSuccess);
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Países</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Países
</h1>
<p class="text-muted-foreground">
Gestiona los países disponibles en el sistema
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
<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 onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo País
</Button>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar país por nombre o clave..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
{#if error}
<div class="rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<div class="rounded-md border">
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable
data={allItems}
{columns}
@@ -157,5 +194,3 @@
/>
</div>
</div>
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,55 +3,29 @@
import { currencyTypesApi, type CurrencyType } from '$lib/api/dashboard/reference_data/currency_types';
import DataTable from '$lib/components/dashboard/reference_data/currency_types/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/currency_types/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/currency_types/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/currency_types/list';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Atajos
useShortcuts(
'Lista Tipos de Moneda',
obtenerAtajosLista({
manejarNuevo: () => (showCreateDialog = true),
manejarActualizar: reloadData
})
);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -64,98 +38,116 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await currencyTypesApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await currencyTypesApi.list(currentPage + 1, pageSize);
try {
const response = await currencyTypesApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Crear columnas con el callback onSuccess
useShortcuts('Tipos de Moneda', [
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
const columns = createColumns(handleSuccess);
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Tipos de Moneda</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Tipos de Moneda
</h1>
<p class="text-muted-foreground">
Gestiona los tipos de moneda disponibles en el sistema
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
<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 onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Tipo de Moneda
</Button>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por clave, moneda o país..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
{#if error}
<div class="rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<div class="rounded-md border">
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,46 +3,29 @@
import { customsSectionsApi, type CustomsSection } from '$lib/api/dashboard/reference_data/customs_sections';
import DataTable from '$lib/components/dashboard/reference_data/customs_sections/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/customs_sections/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/customs_sections/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -55,122 +38,116 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await customsSectionsApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await customsSectionsApi.list(currentPage + 1, pageSize);
const response = await customsSectionsApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Keyboard Shortcuts
useShortcuts('Secciones Aduanales', [
{
key: 'Alt+Shift+N',
description: 'Nueva Sección',
action: handleCreateClick
},
{
key: 'Alt+Shift+R',
description: 'Actualizar Lista',
action: reloadData
}
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
// Crear columnas con el callback onSuccess
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Secciones Aduanales</h1>
<p class="text-muted-foreground">Gestiona las secciones aduanales del sistema</p>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Secciones Aduanales
</h1>
<p class="text-muted-foreground">
Gestiona las secciones aduanales del sistema
</p>
</div>
<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>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por código o nombre de sección..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nueva Sección
</Button>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Secciones Aduanales</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</Card.Content>
</Card.Root>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,55 +3,29 @@
import { customsWarehousesApi, type CustomsWarehouse } from '$lib/api/dashboard/reference_data/customs_warehouses';
import DataTable from '$lib/components/dashboard/reference_data/customs_warehouses/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/customs_warehouses/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/customs_warehouses/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/customs_warehouses/list';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Atajos
useShortcuts(
'Lista Almacenes Aduanales',
obtenerAtajosLista({
manejarNuevo: () => (showCreateDialog = true),
manejarActualizar: reloadData
})
);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -64,98 +38,116 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await customsWarehousesApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await customsWarehousesApi.list(currentPage + 1, pageSize);
try {
const response = await customsWarehousesApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Crear columnas con el callback onSuccess
useShortcuts('Almacenes Aduanales', [
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
const columns = createColumns(handleSuccess);
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Recintos Fiscalizados</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Recintos Fiscalizados
</h1>
<p class="text-muted-foreground">
Gestiona los recintos fiscalizados del sistema aduanal
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
<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 onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Recinto
</Button>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por clave o nombre de recinto..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
{#if error}
<div class="rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<div class="rounded-md border">
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -18,13 +18,20 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
}
try {
// Obtener parámetros de paginación de la URL
// Obtener parámetros de paginación y filtros de la URL
const page = parseInt(url.searchParams.get('page') || '1');
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
const code = url.searchParams.get('code') || '';
const description = url.searchParams.get('description') || '';
// Construir URL con filtros
let endpoint = `v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}`;
if (code) endpoint += `&code=${encodeURIComponent(code)}`;
if (description) endpoint += `&description=${encodeURIComponent(description)}`;
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
const response = await authenticatedFetch(
`v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}`,
endpoint,
{},
cookies,
fetch

View File

@@ -2,29 +2,75 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { incotermsApi, type Incoterm } from '$lib/api/dashboard/reference_data/incoterms';
import { createColumns } from '$lib/components/dashboard/reference_data/incoterms/columns';
import DataTable from '$lib/components/dashboard/reference_data/incoterms/data-table.svelte';
import CreateEditDialog from '$lib/components/dashboard/reference_data/incoterms/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { RefreshCw, Plus } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/incoterms/list';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
// Atajos
useShortcuts(
'Lista Incoterms',
obtenerAtajosLista({
manejarNuevo: () => (createDialogOpen = true),
manejarActualizar: handleSuccess
manejarActualizar: reloadData
})
);
// Estado para infinite scroll
let allItems = $state<Incoterm[]>(data.items || []);
let currentPage = $state(data.page || 1);
let pageSize = $state(50);
let totalItems = $state(data.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await incotermsApi.list(currentPage + 1, pageSize, searchCode, searchDesc);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
window.location.reload();
}
// Filtros
let searchCode = $state($page.url.searchParams.get('code') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
@@ -33,7 +79,22 @@
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = setTimeout(async () => {
loading = true;
try {
const response = await incotermsApi.list(1, pageSize, searchCode, searchDesc);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
// Actualizar URL silenciosamente para mantener estado
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
@@ -41,8 +102,7 @@
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);
}
@@ -52,44 +112,61 @@
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Incoterms</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Incoterms
</h1>
<p class="text-muted-foreground">
Catálogo de Incoterms
Catálogo de términos internacionales de comercio
</p>
</div>
</div>
<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>
</div>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-72 items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Clave</span>
<Input
placeholder="Buscar por clave..."
placeholder="Filtro por código..."
bind:value={searchCode}
oninput={handleSearch}
class="h-9"
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<div class="grid flex-1 items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Descripción</span>
<Input
placeholder="Buscar por descripción..."
placeholder="Buscar por descripción del término..."
bind:value={searchDesc}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
<div class="rounded-md border">
{#if error}
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable
columns={createColumns(handleSuccess)}
data={data.items || []}
pageCount={Math.ceil((data.total || 0) / (data.page_size || 50))}
totalItems={data.total || 0}
data={allItems}
{loading}
{hasMore}
{loadMore}
/>
</div>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={handleSuccess}
/>
</div>

View File

@@ -3,55 +3,29 @@
import { invoiceTypesApi, type InvoiceType } from '$lib/api/dashboard/reference_data/invoice_types';
import DataTable from '$lib/components/dashboard/reference_data/invoice_types/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/invoice_types/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/invoice_types/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/invoice_types/list';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Atajos
useShortcuts(
'Lista Tipos de Factura',
obtenerAtajosLista({
manejarNuevo: () => (showCreateDialog = true),
manejarActualizar: reloadData
})
);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -64,98 +38,116 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await invoiceTypesApi.list(1, pageSize, undefined, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await invoiceTypesApi.list(currentPage + 1, pageSize);
try {
const response = await invoiceTypesApi.list(currentPage + 1, pageSize, undefined, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Crear columnas con el callback onSuccess
useShortcuts('Tipos de Factura', [
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
const columns = createColumns(handleSuccess);
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Tipos de Factura</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Tipos de Factura
</h1>
<p class="text-muted-foreground">
Gestiona los tipos de facturas del sistema
Gestiona los tipos de facturas disponibles en el sistema
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
<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 onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Tipo de Factura
</Button>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por clave, descripción o nota..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
{#if error}
<div class="rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<div class="rounded-md border">
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,56 +3,29 @@
import { materialTypesApi, type MaterialType } from '$lib/api/dashboard/reference_data/material_types';
import DataTable from '$lib/components/dashboard/reference_data/material_types/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/material_types/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/material_types/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/material_types/list';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Atajos
useShortcuts(
'Lista Tipos de Material',
obtenerAtajosLista({
manejarNuevo: () => (showCreateDialog = true),
manejarActualizar: reloadData
})
);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -65,116 +38,116 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await materialTypesApi.list(1, pageSize, undefined, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await materialTypesApi.list(currentPage + 1, pageSize);
try {
const response = await materialTypesApi.list(currentPage + 1, pageSize, undefined, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Crear columnas con el callback onSuccess
useShortcuts('Tipos de Material', [
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Tipos de Material</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Tipos de Material
</h1>
<p class="text-muted-foreground">
Gestiona los tipos de materiales del sistema
Gestiona los tipos de materiales disponibles en el sistema
</p>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Tipo de Material
</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>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por clave o descripción..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Tipos de Material</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,46 +3,29 @@
import { paymentMethodsApi, type PaymentMethod } from '$lib/api/dashboard/reference_data/payment_methods';
import DataTable from '$lib/components/dashboard/reference_data/payment_methods/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/payment_methods/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/payment_methods/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -55,122 +38,116 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await paymentMethodsApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await paymentMethodsApi.list(currentPage + 1, pageSize);
const response = await paymentMethodsApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Keyboard Shortcuts
useShortcuts('Métodos de Pago', [
{
key: 'Alt+Shift+N',
description: 'Nuevo Método',
action: handleCreateClick
},
{
key: 'Alt+Shift+R',
description: 'Actualizar Lista',
action: reloadData
}
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
// Crear columnas con el callback onSuccess
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Métodos de Pago</h1>
<p class="text-muted-foreground">Gestiona las formas de pago disponibles en el sistema</p>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Métodos de Pago
</h1>
<p class="text-muted-foreground">
Gestiona las formas de pago disponibles en el sistema
</p>
</div>
<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>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por clave o descripción de pago..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Método de Pago
</Button>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Métodos de Pago</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</Card.Content>
</Card.Root>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,46 +3,29 @@
import { pedimentoCodesApi, type PedimentoCode } from '$lib/api/dashboard/reference_data/pedimento_codes';
import DataTable from '$lib/components/dashboard/reference_data/pedimento_codes/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/pedimento_codes/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/pedimento_codes/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
import { browser } from '$app/environment';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -55,122 +38,116 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await pedimentoCodesApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await pedimentoCodesApi.list(currentPage + 1, pageSize);
const response = await pedimentoCodesApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Keyboard Shortcuts
useShortcuts('Claves de Pedimento', [
{
key: 'Alt+Shift+N',
description: 'Nueva Clave',
action: handleCreateClick
},
{
key: 'Alt+Shift+R',
description: 'Actualizar Lista',
action: reloadData
}
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
// Crear columnas con el callback onSuccess
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Claves de Pedimento</h1>
<p class="text-muted-foreground">Gestiona las claves de pedimento del sistema aduanero</p>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Claves de Pedimento
</h1>
<p class="text-muted-foreground">
Gestiona las claves de pedimento del sistema aduanero
</p>
</div>
<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>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por clave o descripción..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Código de Pedimento
</Button>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Claves de Pedimento</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</Card.Content>
</Card.Root>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,46 +3,29 @@
import { pedimentoRegimensApi, type PedimentoRegimen } from '$lib/api/dashboard/reference_data/pedimento_regimens';
import DataTable from '$lib/components/dashboard/reference_data/pedimento_regimens/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/pedimento_regimens/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/pedimento_regimens/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -55,122 +38,116 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await pedimentoRegimensApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await pedimentoRegimensApi.list(currentPage + 1, pageSize);
const response = await pedimentoRegimensApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Keyboard Shortcuts
useShortcuts('Regímenes', [
{
key: 'Alt+Shift+N',
description: 'Nuevo Régimen',
action: handleCreateClick
},
{
key: 'Alt+Shift+R',
description: 'Actualizar Lista',
action: reloadData
}
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
// Crear columnas con el callback onSuccess
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Regímenes de Pedimento</h1>
<p class="text-muted-foreground">Gestiona los regímenes aduaneros de pedimento</p>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Regímenes
</h1>
<p class="text-muted-foreground">
Gestiona los regímenes aduaneros de pedimento
</p>
</div>
<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>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por código o descripción de régimen..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Régimen de Pedimento
</Button>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Regímenes de Pedimento</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</Card.Content>
</Card.Root>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -4,56 +4,29 @@
import { companyStore } from '$lib/stores/company.svelte';
import DataTable from '$lib/components/dashboard/reference_data/sectors/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/sectors/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/sectors/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/sectors/list';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Atajos
useShortcuts(
'Lista Sectores',
obtenerAtajosLista({
manejarNuevo: () => (showCreateDialog = true),
manejarActualizar: reloadData
})
);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -66,121 +39,139 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function getActiveCompanyId(): number | null {
const fromStore = companyStore.activeCompany?.id;
if (fromStore) return fromStore;
if (!browser) 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;
}
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const companyId = getActiveCompanyId();
if (!companyId) {
error = 'No hay empresa activa seleccionada';
return;
}
const response = await sectorsApi.list(1, pageSize, companyId, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
error = 'No hay empresa activa seleccionada';
return;
}
const response = await sectorsApi.list(currentPage + 1, pageSize, companyId);
try {
const companyId = getActiveCompanyId();
if (!companyId) {
error = 'No hay empresa activa seleccionada';
return;
}
const response = await sectorsApi.list(currentPage + 1, pageSize, companyId, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Crear columnas con el callback onSuccess
useShortcuts('Sectores', [
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Sectores</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Sectores
</h1>
<p class="text-muted-foreground">
Gestiona los sectores económicos
Gestiona los sectores económicos del sistema
</p>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Sector
</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>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda por Clave</span>
<Input
placeholder="Buscar sector por clave..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Sectores</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,29 +3,25 @@
import { statesApi, type State } from '$lib/api/dashboard/reference_data/states';
import DataTable from '$lib/components/dashboard/reference_data/states/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/states/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/states/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 type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/states/list';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
import { page } from '$app/stores';
import { RefreshCw } from 'lucide-svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Atajos
useShortcuts(
'Lista Estados',
obtenerAtajosLista({
manejarNuevo: () => (showCreateDialog = true),
manejarActualizar: reloadData
})
);
// Keyboard Shortcuts
useShortcuts('Estados', [
{
key: 'Alt+Shift+R',
description: 'Actualizar Lista',
action: reloadData
}
]);
// Sincronizar token de cookies a localStorage al montar el componente
onMount(() => {
@@ -65,6 +61,34 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await statesApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
@@ -72,7 +96,7 @@
error = null;
try {
const response = await statesApi.list(currentPage + 1, pageSize);
const response = await statesApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
@@ -109,10 +133,6 @@
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
@@ -122,59 +142,52 @@
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Estados</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Estados
</h1>
<p class="text-muted-foreground">
Gestiona los estados y sus claves de identificación
</p>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Estado
</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>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por nombre o clave de estado..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Estados</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</div>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,56 +3,29 @@
import { transportModesApi, type TransportMode } from '$lib/api/dashboard/reference_data/transport_modes';
import DataTable from '$lib/components/dashboard/reference_data/transport_modes/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/transport_modes/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/transport_modes/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/transport_modes/list';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Atajos
useShortcuts(
'Lista Modos de Transporte',
obtenerAtajosLista({
manejarNuevo: () => (showCreateDialog = true),
manejarActualizar: reloadData
})
);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -65,116 +38,116 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await transportModesApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await transportModesApi.list(currentPage + 1, pageSize);
try {
const response = await transportModesApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Crear columnas con el callback onSuccess
useShortcuts('Modos de Transporte', [
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Modos de Transporte</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Modos de Transporte
</h1>
<p class="text-muted-foreground">
Gestiona los modos de transporte disponibles
Gestiona los modos de transporte disponibles en el sistema
</p>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Modo de Transporte
</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>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por clave o nombre..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Modos de Transporte</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,56 +3,29 @@
import { transportTypesApi, type TransportType } from '$lib/api/dashboard/reference_data/transport_types';
import DataTable from '$lib/components/dashboard/reference_data/transport_types/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/transport_types/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/transport_types/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/transport_types/list';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Atajos
useShortcuts(
'Lista Tipos de Transporte',
obtenerAtajosLista({
manejarNuevo: () => (showCreateDialog = true),
manejarActualizar: reloadData
})
);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -65,116 +38,116 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await transportTypesApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await transportTypesApi.list(currentPage + 1, pageSize);
try {
const response = await transportTypesApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Crear columnas con el callback onSuccess
useShortcuts('Tipos de Transporte', [
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Tipos de Transporte</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Tipos de Transporte
</h1>
<p class="text-muted-foreground">
Gestiona los tipos de transporte según código SAT
</p>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nuevo Tipo de Transporte
</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>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por código o descripción..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Tipos de Transporte</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -3,55 +3,29 @@
import { valuationMethodsApi, type ValuationMethod } from '$lib/api/dashboard/reference_data/valuation_methods';
import DataTable from '$lib/components/dashboard/reference_data/valuation_methods/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/reference_data/valuation_methods/columns.js';
import CreateEditDialog from '$lib/components/dashboard/reference_data/valuation_methods/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/valuation_methods/list';
import type { PageData } from './$types';
import { Input } from '$lib/components/ui/input';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { RefreshCw } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import type { PageData } from './$types';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Atajos
useShortcuts(
'Lista Métodos de Valoración',
obtenerAtajosLista({
manejarNuevo: () => (showCreateDialog = true),
manejarActualizar: reloadData
})
);
// Sincronizar token de cookies a localStorage al montar el componente
// Sincronizar token de cookies
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}
});
@@ -64,98 +38,116 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Filtros
let searchQuery = $state($page.url.searchParams.get('search') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(async () => {
loading = true;
try {
const response = await valuationMethodsApi.list(1, pageSize, searchQuery);
if (!response.error && response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
console.error('Error aplicando filtros:', e);
} finally {
loading = false;
}
const url = new URL($page.url);
if (searchQuery) url.searchParams.set('search', searchQuery);
else url.searchParams.delete('search');
history.replaceState(history.state, '', url);
}, 500);
}
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const response = await valuationMethodsApi.list(currentPage + 1, pageSize);
try {
const response = await valuationMethodsApi.list(currentPage + 1, pageSize, searchQuery);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
// Recargar automáticamente después de 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Page] Error loading more:', e);
} finally {
loading = false;
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Crear columnas con el callback onSuccess
useShortcuts('Métodos de Valoración', [
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
]);
const columns = createColumns(handleSuccess);
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Métodos de Valoración</h1>
<div class="space-y-6 p-6">
<!-- Header Section -->
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight">
Métodos de Valoración
</h1>
<p class="text-muted-foreground">
Gestiona los métodos de valoración aduanera
Gestiona los métodos de valoración aduanera autorizados
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" onclick={reloadData}>
<RefreshCw size={16} class="mr-2" />
<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 onclick={handleCreateClick}>
<Plus size={16} class="mr-2" />
Nuevo Método de Valoración
</Button>
</div>
</div>
<!-- Filters Section -->
<div class="flex flex-wrap items-end gap-4">
<div class="grid w-full max-w-sm items-center gap-1.5 font-medium">
<span class="text-xs text-muted-foreground px-1">Búsqueda General</span>
<Input
placeholder="Buscar por clave o descripción..."
bind:value={searchQuery}
oninput={handleSearch}
class="h-9"
/>
</div>
</div>
{#if error}
<div class="rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
<div class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<div class="rounded-md border">
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
<!-- Table Container -->
<div class="rounded-md border bg-background overflow-hidden">
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
</div>
</div>
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />