feature/catalog-fixed-fix-styles
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()}`
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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()}`
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user