feature/homogenizacion-estilos-catalogos-fijos-y-generales
This commit is contained in:
@@ -121,3 +121,45 @@
|
||||
@apply bg-background text-foreground overflow-x-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.catalog-table-shell {
|
||||
@apply rounded-md border border-border/80 bg-card shadow-sm;
|
||||
}
|
||||
|
||||
.catalog-table-scroll {
|
||||
@apply relative w-full flex-1 overflow-auto bg-card;
|
||||
}
|
||||
|
||||
.catalog-table-header {
|
||||
@apply sticky top-0 z-20 border-b border-border/80 bg-card/95 shadow-sm backdrop-blur-md;
|
||||
}
|
||||
|
||||
.catalog-table-head-cell {
|
||||
@apply whitespace-nowrap text-sm font-semibold text-foreground/90;
|
||||
}
|
||||
|
||||
.catalog-table-row {
|
||||
@apply transition-colors hover:bg-accent/35;
|
||||
}
|
||||
|
||||
.catalog-table-row-selected {
|
||||
@apply bg-accent/65 text-accent-foreground hover:bg-accent/65;
|
||||
}
|
||||
|
||||
.catalog-table-sticky-left {
|
||||
@apply sticky left-0 border-r border-border/70 bg-card shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)];
|
||||
}
|
||||
|
||||
.catalog-table-sticky-right {
|
||||
@apply sticky right-0 border-l border-border/70 bg-card shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.08)] dark:shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.25)];
|
||||
}
|
||||
|
||||
.catalog-table-sticky-row-hover {
|
||||
@apply bg-card group-hover/inv-list:bg-accent/35;
|
||||
}
|
||||
|
||||
.catalog-table-sticky-row-selected {
|
||||
@apply bg-accent/65 text-accent-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,14 @@ import type { ApiResponse } from '$lib/api';
|
||||
export async function getMultiCurrencyTypes(
|
||||
companyId: number,
|
||||
page?: number,
|
||||
pageSize?: number
|
||||
pageSize?: number,
|
||||
filters?: { currency_type_code?: string; country_key?: string }
|
||||
): Promise<ApiResponse<MultiCurrencyTypeListResponse>> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
if (page) params.append('page', page.toString());
|
||||
if (pageSize) params.append('page_size', pageSize.toString());
|
||||
if (filters?.currency_type_code) params.append('currency_type_code', filters.currency_type_code);
|
||||
if (filters?.country_key) params.append('country_key', filters.country_key);
|
||||
|
||||
return api.get<MultiCurrencyTypeListResponse>(`/v1/a76/multi-currency-types/?${params.toString()}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import { onMount } from 'svelte';
|
||||
import { type ColumnDef, getCoreRowModel } from '@tanstack/table-core';
|
||||
import { createSvelteTable, FlexRender } from '$lib/components/ui/data-table/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
|
||||
type InfiniteDataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
loading: boolean;
|
||||
hasMore: boolean;
|
||||
loadMore: () => void;
|
||||
emptyMessage?: string;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
loading,
|
||||
hasMore,
|
||||
loadMore,
|
||||
emptyMessage = 'No hay resultados.'
|
||||
}: InfiniteDataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel()
|
||||
});
|
||||
|
||||
let scrollContainer = $state<HTMLDivElement>();
|
||||
let loadingTrigger = $state<HTMLDivElement>();
|
||||
|
||||
onMount(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const [entry] = entries;
|
||||
if (entry.isIntersecting && hasMore && !loading) {
|
||||
loadMore();
|
||||
}
|
||||
},
|
||||
{
|
||||
root: scrollContainer,
|
||||
threshold: 0.1
|
||||
}
|
||||
);
|
||||
|
||||
if (loadingTrigger) {
|
||||
observer.observe(loadingTrigger);
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-full w-full flex-col overflow-hidden">
|
||||
<div
|
||||
class="catalog-table-scroll min-h-[320px] max-h-[calc(100svh-280px)]"
|
||||
bind:this={scrollContainer}
|
||||
>
|
||||
<Table.Root>
|
||||
<Table.Header class="catalog-table-header">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
{@const headerList = headerGroup.headers}
|
||||
{@const lastHeaderColId = headerList[headerList.length - 1]?.column.id}
|
||||
<Table.Row>
|
||||
{#each headerList as header (header.id)}
|
||||
{@const colId = header.column.id}
|
||||
<Table.Head
|
||||
class={[
|
||||
'catalog-table-head-cell',
|
||||
colId === lastHeaderColId &&
|
||||
'catalog-table-sticky-right z-30'
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if table.getRowModel().rows.length}
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
{@const visibleCells = row.getVisibleCells()}
|
||||
{@const lastCellColId = visibleCells[visibleCells.length - 1]?.column.id}
|
||||
<Table.Row
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
class="catalog-table-row"
|
||||
>
|
||||
{#each visibleCells as cell (cell.id)}
|
||||
{@const colId = cell.column.id}
|
||||
<Table.Cell
|
||||
class={[
|
||||
'whitespace-nowrap',
|
||||
colId === lastCellColId &&
|
||||
'catalog-table-sticky-right z-10'
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center text-sm text-muted-foreground">
|
||||
{emptyMessage}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
|
||||
{#if hasMore}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center p-0">
|
||||
<div bind:this={loadingTrigger} class="flex h-full w-full items-center justify-center">
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center gap-3 rounded-full border bg-muted/30 px-6 py-2 shadow-sm">
|
||||
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
|
||||
<span class="text-sm font-medium text-foreground">Cargando más registros...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span class="h-px w-8 bg-border"></span>
|
||||
<span>Desplázate para cargar más</span>
|
||||
<span class="h-px w-8 bg-border"></span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,17 +67,18 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="w-full h-full">
|
||||
<div
|
||||
class="rounded-md border min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto"
|
||||
class="catalog-table-shell min-h-[360px] max-h-[calc(100svh-420px)] overflow-auto [&_[data-slot=table-container]]:w-full"
|
||||
bind:this={scrollContainer}
|
||||
>
|
||||
<Table.Root>
|
||||
<Table.Header class="bg-background">
|
||||
<div class="min-w-max">
|
||||
<Table.Root>
|
||||
<Table.Header class="catalog-table-header">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell">
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
@@ -94,9 +95,7 @@
|
||||
<Table.Row
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
onclick={() => onRowClick?.(row.original)}
|
||||
class="cursor-pointer transition-colors {row.getIsSelected()
|
||||
? 'bg-gray-300 dark:bg-gray-600'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
class="cursor-pointer {row.getIsSelected() ? 'catalog-table-row-selected' : 'catalog-table-row'}"
|
||||
>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
@@ -106,7 +105,7 @@
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center text-sm text-muted-foreground">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
@@ -131,6 +130,7 @@
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</Table.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,67 +38,69 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if table.getRowModel().rows.length}
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<div class="flex h-full w-full flex-col overflow-hidden">
|
||||
<div class="relative flex-1 overflow-auto bg-card shadow-inner">
|
||||
<Table.Root>
|
||||
<Table.Header class="sticky top-0 z-20 border-b bg-background/95 shadow-sm backdrop-blur-md">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head class="whitespace-nowrap">
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if table.getRowModel().rows.length}
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell class="whitespace-nowrap">
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 border-t px-4 py-3">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -40,67 +40,69 @@
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</script>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
<div class="flex h-full w-full flex-col overflow-hidden">
|
||||
<div class="relative flex-1 overflow-auto bg-card shadow-inner">
|
||||
<Table.Root>
|
||||
<Table.Header class="sticky top-0 z-20 border-b bg-background/95 shadow-sm backdrop-blur-md">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head class="whitespace-nowrap">
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell class="whitespace-nowrap">
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
<div class="flex items-center justify-end space-x-2 border-t px-4 py-3">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { untrack } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getSectors, type Sector } from '$lib/api/dashboard/general_catalogs/sectors';
|
||||
import { Loader2, Search } from 'lucide-svelte';
|
||||
@@ -103,34 +102,39 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-3xl font-bold tracking-tight">{title}</h2>
|
||||
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex w-full items-center gap-4">
|
||||
<div class="relative flex-1">
|
||||
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar por clave o descripción..."
|
||||
class="pl-8"
|
||||
bind:value={searchTerm}
|
||||
oninput={handleSearchInput}
|
||||
onkeydown={handleKeyDown}
|
||||
/>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Sectores</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="relative">
|
||||
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar por clave o descripción..."
|
||||
class="h-9 w-44 bg-card pl-8 lg:w-64"
|
||||
bind:value={searchTerm}
|
||||
oninput={handleSearchInput}
|
||||
onkeydown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="rounded-md border">
|
||||
<Card.Content class="min-h-0 p-0">
|
||||
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="min-h-0 flex-1 overflow-auto">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Header class="catalog-table-header">
|
||||
<Table.Row>
|
||||
<Table.Head class="w-[100px]">Clave</Table.Head>
|
||||
<Table.Head>Descripción</Table.Head>
|
||||
<Table.Head class="text-right">Estatus</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell w-[100px]">Clave</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell">Descripción</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell text-right">Estatus</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
@@ -150,7 +154,7 @@
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each sectors as sector}
|
||||
<Table.Row>
|
||||
<Table.Row class="catalog-table-row">
|
||||
<Table.Cell class="font-medium">{sector.key}</Table.Cell>
|
||||
<Table.Cell>{sector.description}</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
@@ -174,15 +178,13 @@
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col items-center gap-2">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
Mostrando {sectors.length} de {total} registros
|
||||
</div>
|
||||
<!-- Infinite Scroll Sentinel -->
|
||||
<div bind:this={sentinel} class="h-4 w-full"></div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {sectors.length} de {total} registros</div>
|
||||
|
||||
<!-- Infinite Scroll Sentinel -->
|
||||
<div bind:this={sentinel} class="h-4 w-full"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { untrack } from 'svelte';
|
||||
import {
|
||||
getCanadianFractions,
|
||||
deleteCanadianFraction,
|
||||
@@ -8,6 +8,7 @@
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Search, Loader2, Plus, Pencil, Trash2 } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import CanadianFractionDialog from './CanadianFractionDialog.svelte';
|
||||
@@ -25,6 +26,7 @@
|
||||
let searchTimeout: ReturnType<typeof setTimeout>;
|
||||
let observer: IntersectionObserver;
|
||||
let sentinel: HTMLDivElement;
|
||||
let scrollContainer: HTMLDivElement;
|
||||
|
||||
// Infinite scroll state
|
||||
let hasMore = $state(true);
|
||||
@@ -138,7 +140,7 @@
|
||||
loadFractions(false);
|
||||
}
|
||||
},
|
||||
{ rootMargin: '100px' }
|
||||
{ root: scrollContainer || null, rootMargin: '100px' }
|
||||
);
|
||||
|
||||
if (sentinel) observer.observe(sentinel);
|
||||
@@ -162,101 +164,99 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col items-end justify-between gap-4 md:flex-row">
|
||||
<div class="flex max-w-2xl flex-1 items-end gap-4">
|
||||
<div class="flex-1">
|
||||
<label for="search-fraction" class="mb-2 block text-sm font-medium">Buscar</label>
|
||||
<div class="relative">
|
||||
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="search-fraction"
|
||||
type="text"
|
||||
placeholder="Buscar por fracción o descripción..."
|
||||
class="pl-9"
|
||||
bind:value={searchQuery}
|
||||
oninput={handleSearchInput}
|
||||
onkeydown={handleKeyDown}
|
||||
/>
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden">
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Fracciones Canadienses</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="relative">
|
||||
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="search-fraction"
|
||||
type="text"
|
||||
placeholder="Buscar por fracción o descripción..."
|
||||
class="h-9 w-44 bg-card pl-9 lg:w-64"
|
||||
bind:value={searchQuery}
|
||||
oninput={handleSearchInput}
|
||||
onkeydown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<Button class="h-9" onclick={handleCreate}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Fracción
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button onclick={handleCreate}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Fracción
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Fracción</Table.Head>
|
||||
<Table.Head>Descripción</Table.Head>
|
||||
<Table.Head>País</Table.Head>
|
||||
<Table.Head>Unidad</Table.Head>
|
||||
<Table.Head class="text-right">ADV</Table.Head>
|
||||
<Table.Head class="w-[100px]">Acciones</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if fractions.length === 0 && !loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={6} class="h-24 text-center"
|
||||
>No se encontraron resultados</Table.Cell
|
||||
>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each fractions as fraction}
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0">
|
||||
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-auto" bind:this={scrollContainer}>
|
||||
<Table.Root>
|
||||
<Table.Header class="catalog-table-header">
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-medium">{fraction.fraction}</Table.Cell>
|
||||
<Table.Cell>{fraction.description || '-'}</Table.Cell>
|
||||
<Table.Cell>{fraction.country_code}</Table.Cell>
|
||||
<Table.Cell>{fraction.unit_of_measure || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-right">{fraction.ad_valorem ?? '-'}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
onclick={() => handleEdit(fraction)}
|
||||
>
|
||||
<Pencil class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-destructive hover:text-destructive"
|
||||
onclick={() => handleDelete(fraction)}
|
||||
disabled={deletingFractionId === fraction.id}
|
||||
>
|
||||
{#if deletingFractionId === fraction.id}
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 class="h-4 w-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Head class="catalog-table-head-cell">Fracción</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell">Descripción</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell">País</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell">Unidad</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell text-right">ADV</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell w-[100px]">Acciones</Table.Head>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={6} class="h-24 text-center">
|
||||
<div class="flex justify-center">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if fractions.length === 0 && !loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={6} class="h-24 text-center">No se encontraron resultados</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each fractions as fraction}
|
||||
<Table.Row class="catalog-table-row">
|
||||
<Table.Cell class="font-medium">{fraction.fraction}</Table.Cell>
|
||||
<Table.Cell>{fraction.description || '-'}</Table.Cell>
|
||||
<TableCell>{fraction.country_code}</TableCell>
|
||||
<TableCell>{fraction.unit_of_measure || '-'}</TableCell>
|
||||
<TableCell class="text-right">{fraction.ad_valorem ?? '-'}</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => handleEdit(fraction)}>
|
||||
<Pencil class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-destructive hover:text-destructive"
|
||||
onclick={() => handleDelete(fraction)}
|
||||
disabled={deletingFractionId === fraction.id}
|
||||
>
|
||||
{#if deletingFractionId === fraction.id}
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 class="h-4 w-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={6} class="h-24 text-center">
|
||||
<div class="flex justify-center">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
<div bind:this={sentinel} class="h-4 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Infinite Scroll Sentinel -->
|
||||
<div bind:this={sentinel} class="h-4 w-full"></div>
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalItems} registros</div>
|
||||
|
||||
<CanadianFractionDialog
|
||||
bind:open={dialogOpen}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { untrack } from 'svelte';
|
||||
import {
|
||||
getHistoricalFractions,
|
||||
deleteHistoricalFraction,
|
||||
@@ -8,6 +8,7 @@
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Search, Loader2, Plus, Pencil, Trash2 } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import HistoricalFractionDialog from './HistoricalFractionDialog.svelte';
|
||||
@@ -169,11 +170,12 @@ let scrollContainer: HTMLDivElement;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-[calc(100vh-85px)] flex-col space-y-4 p-4">
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-2xl font-bold tracking-tight">{title}</h2>
|
||||
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Gestiona las fracciones históricas de la tarifa.
|
||||
</p>
|
||||
@@ -184,122 +186,100 @@ let scrollContainer: HTMLDivElement;
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="flex flex-col items-end justify-between gap-4 md:flex-row">
|
||||
<div class="flex max-w-2xl flex-1 items-end gap-4">
|
||||
<div class="flex-1">
|
||||
<label
|
||||
for="search-fraction"
|
||||
class="mb-1 block text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
Fracción Histórica
|
||||
</label>
|
||||
<div class="relative">
|
||||
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="search-fraction"
|
||||
type="text"
|
||||
placeholder="Buscar fracción..."
|
||||
class="h-9 pl-9"
|
||||
bind:value={historicalFraction}
|
||||
oninput={handleSearchInput}
|
||||
onkeydown={handleKeyDown}
|
||||
/>
|
||||
<Card.Root class="flex min-h-0 flex-1 flex-col border bg-background">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Fracciones Históricas</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="relative">
|
||||
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="search-fraction"
|
||||
type="text"
|
||||
placeholder="Buscar fracción..."
|
||||
class="h-9 w-40 bg-card pl-9 lg:w-56"
|
||||
bind:value={historicalFraction}
|
||||
oninput={handleSearchInput}
|
||||
onkeydown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0">
|
||||
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-y-auto" bind:this={scrollContainer}>
|
||||
<Table.Root>
|
||||
<Table.Header class="catalog-table-header">
|
||||
<Table.Row>
|
||||
<Table.Head class="catalog-table-head-cell">Fracción</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell">Tipo</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell">UM</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell">País</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell">Fecha Pub.</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell">Fecha Fin</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell text-right">IGI</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell text-right">IGE</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell w-[100px]">Acciones</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if fractions.length === 0 && !loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={9} class="h-24 text-center">No se encontraron resultados</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each fractions as fraction}
|
||||
<Table.Row class="catalog-table-row">
|
||||
<Table.Cell class="font-medium">{fraction.historical_fraction}</Table.Cell>
|
||||
<Table.Cell>{fraction.fraction_type || '-'}</Table.Cell>
|
||||
<Table.Cell>{fraction.unit_of_measure_code || '-'}</Table.Cell>
|
||||
<Table.Cell>{fraction.country || '-'}</Table.Cell>
|
||||
<Table.Cell>{fraction.publication_date ? new Date(fraction.publication_date).toLocaleDateString() : '-'}</Table.Cell>
|
||||
<Table.Cell>{fraction.end_date ? new Date(fraction.end_date).toLocaleDateString() : '-'}</Table.Cell>
|
||||
<Table.Cell class="text-right">{fraction.import_tax_rate ?? '-'}</Table.Cell>
|
||||
<Table.Cell class="text-right">{fraction.export_tax_rate ?? '-'}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => handleEdit(fraction)}>
|
||||
<Pencil class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-destructive hover:text-destructive"
|
||||
onclick={() => handleDelete(fraction)}
|
||||
disabled={deletingFractionId === fraction.id}
|
||||
>
|
||||
{#if deletingFractionId === fraction.id}
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 class="h-4 w-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={9} class="h-24 text-center">
|
||||
<div class="flex justify-center">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
<div bind:this={sentinel} class="h-4 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Tabla con scroll interno -->
|
||||
<div class="flex min-h-0 flex-1 flex-col rounded-md border">
|
||||
<div
|
||||
class="min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto"
|
||||
bind:this={scrollContainer}
|
||||
>
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Fracción</Table.Head>
|
||||
<Table.Head>Tipo</Table.Head>
|
||||
<Table.Head>UM</Table.Head>
|
||||
<Table.Head>País</Table.Head>
|
||||
<Table.Head>Fecha Pub.</Table.Head>
|
||||
<Table.Head>Fecha Fin</Table.Head>
|
||||
<Table.Head class="text-right">IGI</Table.Head>
|
||||
<Table.Head class="text-right">IGE</Table.Head>
|
||||
<Table.Head class="w-[100px]">Acciones</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if fractions.length === 0 && !loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={9} class="h-24 text-center"
|
||||
>No se encontraron resultados</Table.Cell
|
||||
>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each fractions as fraction}
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-medium">{fraction.historical_fraction}</Table.Cell>
|
||||
<Table.Cell>{fraction.fraction_type || '-'}</Table.Cell>
|
||||
<Table.Cell>{fraction.unit_of_measure_code || '-'}</Table.Cell>
|
||||
<Table.Cell>{fraction.country || '-'}</Table.Cell>
|
||||
<Table.Cell
|
||||
>{fraction.publication_date
|
||||
? new Date(fraction.publication_date).toLocaleDateString()
|
||||
: '-'}</Table.Cell
|
||||
>
|
||||
<Table.Cell
|
||||
>{fraction.end_date
|
||||
? new Date(fraction.end_date).toLocaleDateString()
|
||||
: '-'}</Table.Cell
|
||||
>
|
||||
<Table.Cell class="text-right">{fraction.import_tax_rate ?? '-'}</Table.Cell>
|
||||
<Table.Cell class="text-right">{fraction.export_tax_rate ?? '-'}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
onclick={() => handleEdit(fraction)}
|
||||
>
|
||||
<Pencil class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-destructive hover:text-destructive"
|
||||
onclick={() => handleDelete(fraction)}
|
||||
disabled={deletingFractionId === fraction.id}
|
||||
>
|
||||
{#if deletingFractionId === fraction.id}
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 class="h-4 w-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={9} class="h-24 text-center">
|
||||
<div class="flex justify-center">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
|
||||
<!-- Infinite Scroll Sentinel dentro del contenedor con scroll -->
|
||||
<div bind:this={sentinel} class="h-4 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalItems} registros</div>
|
||||
|
||||
<HistoricalFractionDialog
|
||||
bind:open={dialogOpen}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -17,7 +17,7 @@
|
||||
type TariffFraction
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { untrack } from 'svelte';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import TariffFractionFormDialog from './TariffFractionFormDialog.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -43,6 +43,7 @@
|
||||
let searchTimeout: ReturnType<typeof setTimeout>;
|
||||
let observer: IntersectionObserver;
|
||||
let sentinel: HTMLDivElement;
|
||||
let scrollContainer: HTMLDivElement;
|
||||
|
||||
// Infinite scroll state
|
||||
let hasMore = $state(true);
|
||||
@@ -121,7 +122,7 @@
|
||||
handlePageChange(currentPage + 1);
|
||||
}
|
||||
},
|
||||
{ rootMargin: '100px' }
|
||||
{ root: scrollContainer || null, rootMargin: '100px' }
|
||||
);
|
||||
|
||||
if (sentinel) observer.observe(sentinel);
|
||||
@@ -180,108 +181,118 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-bold tracking-tight">{title}</h2>
|
||||
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
|
||||
{#if !readOnly}
|
||||
<Button onclick={openCreateDialog}>
|
||||
<Button class="h-9" onclick={openCreateDialog}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Fracción
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="relative max-w-sm flex-1">
|
||||
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="Buscar..." class="pl-8" bind:value={search} oninput={handleSearchInput} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Clave</TableHead>
|
||||
<TableHead>Fracción</TableHead>
|
||||
<TableHead>Descripción</TableHead>
|
||||
{#if catalog === 'mex'}
|
||||
<TableHead>NICO</TableHead>
|
||||
<TableHead>U.M.T</TableHead>
|
||||
{:else}
|
||||
<TableHead>Unidad</TableHead>
|
||||
{/if}
|
||||
<TableHead>Adv. Impo</TableHead>
|
||||
<TableHead>Adv. Expo</TableHead>
|
||||
{#if !readOnly}
|
||||
<TableHead class="w-[100px]">Acciones</TableHead>
|
||||
{/if}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#if fractions.length === 0 && !isLoading}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
|
||||
class="h-24 text-center"
|
||||
>
|
||||
No se encontraron resultados
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{:else}
|
||||
{#each fractions as fraction}
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Fracciones</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="relative">
|
||||
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="Buscar..." class="h-9 w-40 bg-card pl-8 lg:w-56" bind:value={search} oninput={handleSearchInput} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0">
|
||||
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-auto" bind:this={scrollContainer}>
|
||||
<Table>
|
||||
<TableHeader class="catalog-table-header">
|
||||
<TableRow>
|
||||
<TableCell class="font-mono">{fraction.um_code || fraction.code}</TableCell>
|
||||
<TableCell class="font-medium">{fraction.fraction}</TableCell>
|
||||
<TableCell class="max-w-md truncate" title={fraction.description}>
|
||||
{fraction.description}
|
||||
</TableCell>
|
||||
<TableHead class="catalog-table-head-cell">Clave</TableHead>
|
||||
<TableHead class="catalog-table-head-cell">Fracción</TableHead>
|
||||
<TableHead class="catalog-table-head-cell">Descripción</TableHead>
|
||||
{#if catalog === 'mex'}
|
||||
<TableCell>{fraction.nico || '-'}</TableCell>
|
||||
<TableCell>{fraction.umt || '-'}</TableCell>
|
||||
<TableHead class="catalog-table-head-cell">NICO</TableHead>
|
||||
<TableHead class="catalog-table-head-cell">U.M.T</TableHead>
|
||||
{:else}
|
||||
<TableCell>{fraction.umt || '-'}</TableCell>
|
||||
<TableHead class="catalog-table-head-cell">Unidad</TableHead>
|
||||
{/if}
|
||||
<TableCell>{fraction.adv_impo || '-'}</TableCell>
|
||||
<TableCell>{fraction.adv_expo || '-'}</TableCell>
|
||||
<TableHead class="catalog-table-head-cell">Adv. Impo</TableHead>
|
||||
<TableHead class="catalog-table-head-cell">Adv. Expo</TableHead>
|
||||
{#if !readOnly}
|
||||
<TableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onclick={() => openEditDialog(fraction)}>
|
||||
<Edit class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="text-destructive hover:text-destructive"
|
||||
onclick={() => confirmDelete(fraction)}
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableHead class="catalog-table-head-cell w-[100px]">Acciones</TableHead>
|
||||
{/if}
|
||||
</TableRow>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if isLoading}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
|
||||
class="h-24 text-center"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#if fractions.length === 0 && !isLoading}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
|
||||
class="h-24 text-center"
|
||||
>
|
||||
No se encontraron resultados
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{:else}
|
||||
{#each fractions as fraction}
|
||||
<TableRow class="catalog-table-row">
|
||||
<TableCell class="font-mono">{fraction.um_code || fraction.code}</TableCell>
|
||||
<TableCell class="font-medium">{fraction.fraction}</TableCell>
|
||||
<TableCell class="max-w-md truncate" title={fraction.description}>
|
||||
{fraction.description}
|
||||
</TableCell>
|
||||
{#if catalog === 'mex'}
|
||||
<TableCell>{fraction.nico || '-'}</TableCell>
|
||||
<TableCell>{fraction.umt || '-'}</TableCell>
|
||||
{:else}
|
||||
<TableCell>{fraction.umt || '-'}</TableCell>
|
||||
{/if}
|
||||
<TableCell>{fraction.adv_impo || '-'}</TableCell>
|
||||
<TableCell>{fraction.adv_expo || '-'}</TableCell>
|
||||
{#if !readOnly}
|
||||
<TableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onclick={() => openEditDialog(fraction)}>
|
||||
<Edit class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="text-destructive hover:text-destructive"
|
||||
onclick={() => confirmDelete(fraction)}
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
{/if}
|
||||
</TableRow>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if isLoading}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
|
||||
class="h-24 text-center"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div bind:this={sentinel} class="h-4 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Infinite Scroll Sentinel -->
|
||||
<div bind:this={sentinel} class="h-4 w-full"></div>
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalFractions} registros</div>
|
||||
</div>
|
||||
|
||||
<AlertDialog.Root bind:open={showDeleteConfirm}>
|
||||
|
||||
@@ -99,9 +99,13 @@
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto rounded-md border" bind:this={scrollContainer}>
|
||||
<Table.Root>
|
||||
<Table.Header class="bg-background">
|
||||
<div
|
||||
class="catalog-table-shell min-h-[360px] max-h-[calc(100svh-420px)] overflow-auto [&_[data-slot=table-container]]:overflow-visible [&_[data-slot=table-container]]:w-full"
|
||||
bind:this={scrollContainer}
|
||||
>
|
||||
<div class="min-w-max">
|
||||
<Table.Root>
|
||||
<Table.Header class="catalog-table-header">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
{@const headerList = headerGroup.headers}
|
||||
{@const lastHeaderColId = headerList[headerList.length - 1]?.column.id}
|
||||
@@ -111,9 +115,9 @@
|
||||
<Table.Head
|
||||
class={[
|
||||
colId === 'select' &&
|
||||
'sticky left-0 z-40 min-w-[2.75rem] border-r border-border/70 bg-background shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]',
|
||||
'catalog-table-sticky-left z-40 min-w-[2.75rem]',
|
||||
colId === lastHeaderColId &&
|
||||
'sticky right-0 z-40 border-l border-border/70 bg-background shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.35)]'
|
||||
'catalog-table-sticky-right z-40'
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
@@ -192,9 +196,7 @@
|
||||
{@const lastCellColId = visibleCells[visibleCells.length - 1]?.column.id}
|
||||
<Table.Row
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
class="group/inv-list cursor-pointer transition-colors {row.getIsSelected()
|
||||
? 'bg-gray-300 dark:bg-gray-600'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
class="group/inv-list cursor-pointer {row.getIsSelected() ? 'catalog-table-row-selected' : 'catalog-table-row'}"
|
||||
onclick={() => onRowClick && onRowClick(row.original)}
|
||||
>
|
||||
{#each visibleCells as cell (cell.id)}
|
||||
@@ -202,12 +204,12 @@
|
||||
<Table.Cell
|
||||
class={[
|
||||
colId === 'select' &&
|
||||
'sticky left-0 z-30 min-w-[2.75rem] border-r border-border/70 shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]',
|
||||
'catalog-table-sticky-left z-30 min-w-[2.75rem]',
|
||||
colId === lastCellColId &&
|
||||
'sticky right-0 z-30 border-l border-border/70 shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.35)]',
|
||||
'catalog-table-sticky-right z-30',
|
||||
row.getIsSelected()
|
||||
? 'bg-gray-300 dark:bg-gray-600'
|
||||
: 'bg-background group-hover/inv-list:bg-gray-100 dark:group-hover/inv-list:bg-gray-700'
|
||||
? 'catalog-table-sticky-row-selected'
|
||||
: 'catalog-table-sticky-row-hover'
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
@@ -244,6 +246,7 @@
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</Table.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,18 +98,24 @@
|
||||
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="rounded-md border min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto"
|
||||
class="min-h-[360px] max-h-[calc(100svh-420px)] overflow-auto rounded-md border bg-background [&_[data-slot=table-container]]:overflow-visible [&_[data-slot=table-container]]:w-full"
|
||||
bind:this={scrollContainer}
|
||||
>
|
||||
<Table.Root class="w-full">
|
||||
<Table.Header class="bg-background sticky top-0 z-10">
|
||||
<div class="min-w-max">
|
||||
<Table.Root>
|
||||
<Table.Header class="sticky top-0 z-20 border-b bg-background/95 shadow-sm backdrop-blur-md">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head class="whitespace-nowrap p-0 {header.column.columnDef.meta?.className || ''}">
|
||||
{@const colId = header.column.id}
|
||||
<Table.Head
|
||||
class="whitespace-nowrap text-sm font-semibold text-foreground/90 p-0 {header.column.columnDef.meta?.className || ''} {colId === 'select'
|
||||
? 'sticky left-0 z-40 min-w-[2.75rem] border-r border-border/70 bg-background shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]'
|
||||
: ''}"
|
||||
>
|
||||
{#if !header.isPlaceholder}
|
||||
<button
|
||||
class="group flex h-full w-full items-center gap-2 px-2 py-2 text-left hover:bg-muted/50"
|
||||
class="group flex h-full w-full items-center justify-between gap-2 px-2 py-2 text-left {header.column.getCanSort() ? 'cursor-pointer select-none' : ''}"
|
||||
onclick={header.column.getToggleSortingHandler()}
|
||||
disabled={!header.column.getCanSort()}
|
||||
>
|
||||
@@ -181,10 +187,19 @@
|
||||
}
|
||||
}}
|
||||
ondblclick={() => handleRowDoubleClick(row)}
|
||||
class="cursor-pointer hover:bg-muted/50 transition-colors {row.getIsSelected() ? 'bg-primary/10' : ''}"
|
||||
class="group/inv-list cursor-pointer transition-colors {row.getIsSelected()
|
||||
? 'bg-gray-300 dark:bg-gray-600'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell class="whitespace-nowrap {cell.column.columnDef.meta?.className || ''}">
|
||||
{@const colId = cell.column.id}
|
||||
<Table.Cell
|
||||
class="whitespace-nowrap {cell.column.columnDef.meta?.className || ''} {colId === 'select'
|
||||
? 'sticky left-0 z-30 min-w-[2.75rem] border-r border-border/70 bg-background shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]'
|
||||
: ''} {row.getIsSelected()
|
||||
? 'bg-gray-300 dark:bg-gray-600'
|
||||
: 'bg-background group-hover/inv-list:bg-gray-100 dark:group-hover/inv-list:bg-gray-700'}"
|
||||
>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
@@ -220,6 +235,7 @@
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</Table.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex col h-full w-full overflow-hidden">
|
||||
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
|
||||
<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}>
|
||||
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<table class="w-full caption-bottom text-sm border-collapse">
|
||||
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
let { data, columns, loading = false, hasMore = false, loadMore }: Props = $props();
|
||||
|
||||
let scrollContainer: HTMLDivElement;
|
||||
let observer: IntersectionObserver;
|
||||
let scrollContainer = $state<HTMLDivElement>();
|
||||
let loadingTrigger = $state<HTMLDivElement>();
|
||||
|
||||
let options = $derived<TableOptions<TData>>({
|
||||
get data() {
|
||||
@@ -34,8 +34,7 @@
|
||||
onMount(() => {
|
||||
if (!loadMore) return;
|
||||
|
||||
// Create intersection observer for infinite scroll
|
||||
observer = new IntersectionObserver(
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const [entry] = entries;
|
||||
if (entry.isIntersecting && hasMore && !loading && loadMore) {
|
||||
@@ -48,29 +47,24 @@
|
||||
}
|
||||
);
|
||||
|
||||
// Observe the last row
|
||||
const lastRow = scrollContainer?.querySelector('tbody tr:last-child');
|
||||
if (lastRow) {
|
||||
observer.observe(lastRow);
|
||||
if (loadingTrigger) {
|
||||
observer.observe(loadingTrigger);
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer?.disconnect();
|
||||
observer.disconnect();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="rounded-md border min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto"
|
||||
bind:this={scrollContainer}
|
||||
>
|
||||
<div class="flex h-full w-full flex-col overflow-hidden">
|
||||
<div class="relative flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
||||
<Table.Root>
|
||||
<Table.Header class="bg-background">
|
||||
<Table.Header class="sticky top-0 z-20 border-b bg-background/95 shadow-sm backdrop-blur-md">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
<Table.Head class="whitespace-nowrap">
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
@@ -86,7 +80,7 @@
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<Table.Cell class="whitespace-nowrap">
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
@@ -101,11 +95,24 @@
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
|
||||
{#if loading}
|
||||
|
||||
{#if hasMore}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-12 text-center text-muted-foreground">
|
||||
Cargando...
|
||||
<Table.Cell colspan={columns.length} class="h-24 p-0 text-center">
|
||||
<div bind:this={loadingTrigger} class="flex h-full w-full items-center justify-center">
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center gap-3 rounded-full border bg-muted/30 px-6 py-2 shadow-sm">
|
||||
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
|
||||
<span class="text-sm font-medium text-foreground">Cargando más registros...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span class="h-px w-8 bg-border"></span>
|
||||
<span>Desplázate para cargar más</span>
|
||||
<span class="h-px w-8 bg-border"></span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
|
||||
@@ -187,15 +187,15 @@
|
||||
const brokerColumns = createBrokerColumns(handleActionSuccess);
|
||||
</script>
|
||||
|
||||
<div class="flex h-[calc(100vh-4rem)] flex-col gap-4 p-4 pb-15">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h1 class="text-2xl font-bold">GESTIÓN ADUANAL</h1>
|
||||
<p class="text-sm text-muted-foreground">Administración de Agentes y Secciones Aduanales</p>
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Gestión Aduanal</h1>
|
||||
<p class="text-muted-foreground">Administración de Agentes y Secciones Aduanales</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs.Root bind:value={activeTab} class="flex flex-1 flex-col overflow-hidden">
|
||||
<Tabs.Root bind:value={activeTab} class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Tabs.List class="mb-4 w-full justify-start rounded-none border-b bg-transparent p-0">
|
||||
<Tabs.Trigger
|
||||
value="brokers"
|
||||
@@ -215,8 +215,8 @@
|
||||
value="brokers"
|
||||
class="mt-0 flex flex-1 gap-4 overflow-hidden data-[state=inactive]:hidden"
|
||||
>
|
||||
<div class="flex flex-1 flex-col gap-4 overflow-hidden">
|
||||
<div class="rounded-lg border bg-card">
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden">
|
||||
<div class="rounded-md border bg-background">
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold">Filtros</h2>
|
||||
@@ -228,7 +228,7 @@
|
||||
<Input
|
||||
bind:value={searchName}
|
||||
placeholder="Buscar por nombre..."
|
||||
class="h-9"
|
||||
class="h-9 bg-card"
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
@@ -237,7 +237,7 @@
|
||||
<Input
|
||||
bind:value={searchPatent}
|
||||
placeholder="Num. Patente..."
|
||||
class="h-9"
|
||||
class="h-9 bg-card"
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
@@ -246,8 +246,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 flex-col overflow-hidden rounded-lg border">
|
||||
<div class="flex items-center justify-between border-b bg-muted/30 p-3">
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border bg-background">
|
||||
<div class="flex items-center justify-between border-b bg-background/95 p-3">
|
||||
<h2 class="text-sm font-semibold">Listado</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
@@ -260,7 +260,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto bg-card">
|
||||
<div class="flex-1 overflow-auto bg-background">
|
||||
<BrokerDataTable
|
||||
data={paginatedItems}
|
||||
columns={brokerColumns}
|
||||
@@ -295,9 +295,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-96 flex-none flex-col overflow-hidden rounded-xl border bg-muted/30 shadow-sm"
|
||||
>
|
||||
<div class="flex w-96 flex-none flex-col overflow-hidden rounded-xl border bg-muted/30 shadow-sm">
|
||||
<div class="border-b bg-card p-4">
|
||||
<p class="text-[10px] tracking-widest text-muted-foreground uppercase opacity-80">
|
||||
Detalles del Agente
|
||||
@@ -407,6 +405,8 @@
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
<div class="h-20"></div>
|
||||
</div>
|
||||
<div
|
||||
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/classification_concepts/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/classification_concepts/columns';
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/classification/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaClasificacion } from '$lib/config/shortcuts/dashboard/general_catalogs/classification/list';
|
||||
import { getClassificationConcepts } from '$lib/api/dashboard/a76/general_catalogs/classification-concepts';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -28,17 +32,54 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
|
||||
const columns = $derived(createColumns(handleSuccess));
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.classifications?.items || []);
|
||||
let currentPage = $state(data.classifications?.page || 1);
|
||||
let pageSize = $state(data.classifications?.page_size || 50);
|
||||
let totalItems = $state(data.classifications?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.classifications) {
|
||||
allItems = data.classifications.items || [];
|
||||
currentPage = data.classifications.page || 1;
|
||||
totalItems = data.classifications.total || 0;
|
||||
pageSize = data.classifications.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchClassification) filters.classification = searchClassification;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getClassificationConcepts(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchClassification) url.searchParams.set('classification', searchClassification);
|
||||
else url.searchParams.delete('classification');
|
||||
@@ -46,47 +87,131 @@
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchClassification) filters.classification = searchClassification;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getClassificationConcepts(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more classifications:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchClassification) filters.classification = searchClassification;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getClassificationConcepts(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading classifications:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Clasificaciones de Conceptos</h1>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de clasificaciones de conceptos</p>
|
||||
</div>
|
||||
<Button onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Clasificación
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por clasificación..."
|
||||
bind:value={searchClassification}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button class="h-9" onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Clasificación
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
data={data.classifications?.items || []}
|
||||
{columns}
|
||||
pageCount={data.classifications?.pages || 0}
|
||||
totalItems={data.classifications?.total || 0}
|
||||
/>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Clasificaciones</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Clasificación"
|
||||
bind:value={searchClassification}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-40 bg-card lg:w-52"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Descripción"
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="p-0">
|
||||
<div class="rounded-md border bg-background overflow-hidden">
|
||||
<InfiniteDataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
<CreateDialog
|
||||
bind:open={dialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/company/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/company/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaEmpresa } from '$lib/config/shortcuts/dashboard/general_catalogs/company_information/list';
|
||||
import { getCompanies } from '$lib/api/dashboard/a76/general_catalogs/company';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -24,16 +26,53 @@
|
||||
})
|
||||
);
|
||||
let dialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Filtros
|
||||
let searchName = $state($page.url.searchParams.get('name') || '');
|
||||
let searchRfc = $state($page.url.searchParams.get('rfc') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.companies?.items || []);
|
||||
let currentPage = $state(data.companies?.page || 1);
|
||||
let pageSize = $state(data.companies?.page_size || 50);
|
||||
let totalItems = $state(data.companies?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.companies) {
|
||||
allItems = data.companies.items || [];
|
||||
currentPage = data.companies.page || 1;
|
||||
totalItems = data.companies.total || 0;
|
||||
pageSize = data.companies.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchName) filters.name = searchName;
|
||||
if (searchRfc) filters.rfc = searchRfc;
|
||||
|
||||
const response = await getCompanies(1, pageSize, filters);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchName) url.searchParams.set('name', searchName);
|
||||
else url.searchParams.delete('name');
|
||||
@@ -41,44 +80,118 @@
|
||||
if (searchRfc) url.searchParams.set('rfc', searchRfc);
|
||||
else url.searchParams.delete('rfc');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchName) filters.name = searchName;
|
||||
if (searchRfc) filters.rfc = searchRfc;
|
||||
|
||||
const response = await getCompanies(currentPage + 1, pageSize, filters);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more companies:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchName) filters.name = searchName;
|
||||
if (searchRfc) filters.rfc = searchRfc;
|
||||
|
||||
const response = await getCompanies(1, pageSize, filters);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading companies:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Información de Empresas</h1>
|
||||
<p class="text-muted-foreground">Gestión de información de empresas</p>
|
||||
</div>
|
||||
<Button href="/dashboard/general_catalogs/company_information/edit">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Empresa
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end gap-4">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por nombre..." bind:value={searchName} oninput={handleSearch} />
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por RFC..." bind:value={searchRfc} oninput={handleSearch} />
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button class="h-9" href="/dashboard/general_catalogs/company_information/edit">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Empresa
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.companies?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.companies?.pages || 0}
|
||||
totalItems={data.companies?.total || 0}
|
||||
/>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Empresas</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Nombre"
|
||||
bind:value={searchName}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-40 bg-card lg:w-56"
|
||||
/>
|
||||
<Input
|
||||
placeholder="RFC"
|
||||
bind:value={searchRfc}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-36 bg-card lg:w-44"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="p-0">
|
||||
<div class="rounded-md border bg-background overflow-hidden">
|
||||
<InfiniteDataTable
|
||||
data={allItems}
|
||||
columns={createColumns(handleSuccess)}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/concepts/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/concepts/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/concepts/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaConceptos } from '$lib/config/shortcuts/dashboard/general_catalogs/concepts/list';
|
||||
import { getConcepts } from '$lib/api/dashboard/a76/general_catalogs/concepts';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let dialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -31,10 +35,47 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.concepts?.items || []);
|
||||
let currentPage = $state(data.concepts?.page || 1);
|
||||
let pageSize = $state(data.concepts?.page_size || 50);
|
||||
let totalItems = $state(data.concepts?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.concepts) {
|
||||
allItems = data.concepts.items || [];
|
||||
currentPage = data.concepts.page || 1;
|
||||
totalItems = data.concepts.total || 0;
|
||||
pageSize = data.concepts.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getConcepts(1, pageSize, companyStore.activeCompany.id, filters);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
@@ -42,49 +83,125 @@
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getConcepts(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more concepts:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getConcepts(1, pageSize, companyStore.activeCompany.id, filters);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading concepts:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Conceptos</h1>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de conceptos</p>
|
||||
</div>
|
||||
<Button onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Concepto
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button class="h-9" onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Concepto
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.concepts?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.concepts?.pages || 0}
|
||||
totalItems={data.concepts?.total || 0}
|
||||
/>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Conceptos</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Código"
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-36 bg-card lg:w-44"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Descripción"
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="p-0">
|
||||
<div class="rounded-md border bg-background overflow-hidden">
|
||||
<InfiniteDataTable
|
||||
data={allItems}
|
||||
columns={createColumns(handleSuccess)}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
|
||||
{#if dialogOpen}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/customs_broker_concepts/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/customs_broker_concepts/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/customs_broker_concepts/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaConceptosAA } from '$lib/config/shortcuts/dashboard/general_catalogs/customs_broker_concepts/list';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { getCustomsBrokerConcepts } from '$lib/api/dashboard/a76/general_catalogs/customs-broker-concepts';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const activeCompanyId = $derived(companyStore.activeCompany?.id);
|
||||
let dialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -31,19 +34,54 @@
|
||||
let searchConcept = $state($page.url.searchParams.get('concept') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
// Solo necesitamos una declaración de handleSuccess
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
|
||||
// columns depende de handleSuccess, así que se queda igual
|
||||
const columns = $derived(createColumns(handleSuccess));
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.concepts?.items || []);
|
||||
let currentPage = $state(data.concepts?.page || 1);
|
||||
let pageSize = $state(data.concepts?.page_size || 50);
|
||||
let totalItems = $state(data.concepts?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.concepts) {
|
||||
allItems = data.concepts.items || [];
|
||||
currentPage = data.concepts.page || 1;
|
||||
totalItems = data.concepts.total || 0;
|
||||
pageSize = data.concepts.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchBrokerKey) filters.broker_key = searchBrokerKey;
|
||||
if (searchConcept) filters.concept = searchConcept;
|
||||
|
||||
const response = await getCustomsBrokerConcepts(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchBrokerKey) url.searchParams.set('broker_key', searchBrokerKey);
|
||||
else url.searchParams.delete('broker_key');
|
||||
@@ -51,48 +89,130 @@
|
||||
if (searchConcept) url.searchParams.set('concept', searchConcept);
|
||||
else url.searchParams.delete('concept');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchBrokerKey) filters.broker_key = searchBrokerKey;
|
||||
if (searchConcept) filters.concept = searchConcept;
|
||||
|
||||
const response = await getCustomsBrokerConcepts(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more broker concepts:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchBrokerKey) filters.broker_key = searchBrokerKey;
|
||||
if (searchConcept) filters.concept = searchConcept;
|
||||
|
||||
const response = await getCustomsBrokerConcepts(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading broker concepts:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Conceptos de Agente Aduanal</h1>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de conceptos de agente aduanal</p>
|
||||
</div>
|
||||
<Button onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Concepto
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por clave de agente..."
|
||||
bind:value={searchBrokerKey}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por concepto..."
|
||||
bind:value={searchConcept}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button class="h-9" onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Concepto
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.concepts?.items || []}
|
||||
{columns}
|
||||
pageCount={data.concepts?.pages || 0}
|
||||
totalItems={data.concepts?.total || 0}
|
||||
/>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Conceptos de Agente Aduanal</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Clave agente"
|
||||
bind:value={searchBrokerKey}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-40 bg-card lg:w-52"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Concepto"
|
||||
bind:value={searchConcept}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="p-0">
|
||||
<div class="rounded-md border bg-background overflow-hidden">
|
||||
<InfiniteDataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
|
||||
{#if activeCompanyId}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { getDodas, deleteDoda, type Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import {
|
||||
Plus,
|
||||
@@ -16,6 +15,7 @@
|
||||
LayoutGrid,
|
||||
Printer
|
||||
} from 'lucide-svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/doda/data-table.svelte';
|
||||
@@ -23,7 +23,6 @@
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/doda/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaDoda } from '$lib/config/shortcuts/dashboard/general_catalogs/doda/list';
|
||||
@@ -194,60 +193,45 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">DODA</h1>
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">DODA</h1>
|
||||
<p class="text-muted-foreground">Gestiona tus Documentos de Operación Aduanera (DODA)</p>
|
||||
</div>
|
||||
<Button onclick={handleCreateClick} class="shadow-sm transition-all hover:translate-y-[-1px]">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo DODA
|
||||
</Button>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData} disabled={loading}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button class="h-9" onclick={handleCreateClick}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo DODA
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Root class="flex min-h-0 flex-1 flex-col border bg-background">
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Filtros Avanzados</Card.Title>
|
||||
<Card.Description>Refina tu búsqueda mediante múltiples criterios</Card.Description>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onclick={clearFilters} class="text-muted-foreground">
|
||||
<RotateCcw class="mr-2 h-4 w-4" />
|
||||
Limpiar Filtros
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="search-integration">No. Integración</Label>
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
||||
<Card.Title>Listado de DODA</Card.Title>
|
||||
<div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-[220px_180px_170px_170px_auto] xl:items-center">
|
||||
<div class="relative">
|
||||
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
id="search-integration"
|
||||
placeholder="Buscar integración..."
|
||||
placeholder="Folio"
|
||||
bind:value={filters.integration_number}
|
||||
class="pl-9"
|
||||
class="h-9 bg-card pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="search-patent">Patente</Label>
|
||||
<Input id="search-patent" placeholder="Buscar patente..." bind:value={filters.patent} />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Estatus</Label>
|
||||
<Input placeholder="Patente" bind:value={filters.patent} class="h-9 bg-card" />
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={filters.status}
|
||||
onValueChange={(v) => (filters.status = v)}
|
||||
>
|
||||
<Select.Trigger class="w-full">
|
||||
{filters.status || 'Todos los estatus'}
|
||||
<Select.Trigger class="h-9 w-full bg-card">
|
||||
{filters.status || 'Estatus'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="">Todos</Select.Item>
|
||||
@@ -257,21 +241,17 @@
|
||||
<Select.Item value="ELIMINADO">ELIMINADO</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Operación</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={filters.operation_type}
|
||||
onValueChange={(v) => (filters.operation_type = v)}
|
||||
>
|
||||
<Select.Trigger class="w-full">
|
||||
<Select.Trigger class="h-9 w-full bg-card">
|
||||
{filters.operation_type === 'I'
|
||||
? 'Importación'
|
||||
: filters.operation_type === 'E'
|
||||
? 'Exportación'
|
||||
: 'Todas'}
|
||||
: 'Operación'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="">Todas</Select.Item>
|
||||
@@ -279,38 +259,35 @@
|
||||
<Select.Item value="E">E - Exportación</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={clearFilters}>
|
||||
<RotateCcw class="mr-2 h-4 w-4" />
|
||||
Limpiar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0">
|
||||
<div class="rounded-md border bg-background">
|
||||
<DataTable
|
||||
data={allItems}
|
||||
columns={createColumns(reloadData)}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
{selectedId}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Listado de DODAs</Card.Title>
|
||||
<Card.Description>
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</Card.Description>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onclick={reloadData} disabled={loading}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="p-0">
|
||||
<DataTable
|
||||
data={allItems}
|
||||
columns={createColumns(reloadData)}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
{selectedId}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
<span class="ml-2">•</span>
|
||||
<span class="ml-2">Filtros activos: {Object.values(filters).filter((value) => value !== '').length}</span>
|
||||
</div>
|
||||
|
||||
<div class="h-20"></div>
|
||||
|
||||
<!-- Footer fijo de acciones (estilo Facturas) -->
|
||||
<div
|
||||
@@ -355,9 +332,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-20"></div>
|
||||
<!-- Espacio buffer para el footer -->
|
||||
|
||||
{#if dialogOpen}
|
||||
<CreateEditDialog bind:open={dialogOpen} onSuccess={reloadData} />
|
||||
{/if}
|
||||
|
||||
@@ -262,7 +262,7 @@
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</Button>
|
||||
<h1 class="text-3xl font-bold tracking-tight">{title}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
|
||||
</div>
|
||||
<p class="text-muted-foreground">Catálogos Generales / Doda</p>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
|
||||
import DataTable from '$lib/components/dashboard/transportation/drivers/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/transportation/drivers/create-edit-dialog.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/transportation/drivers/columns';
|
||||
|
||||
@@ -15,13 +16,12 @@
|
||||
|
||||
let data = $state<Driver[]>([]);
|
||||
let totalItems = $state(0);
|
||||
let pageCount = $state(0);
|
||||
let loading = $state(false);
|
||||
let currentPage = $state(1);
|
||||
let pageSize = $state(50);
|
||||
let hasMore = $derived(data.length < totalItems);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
let currentPage = $derived(Number($page.url.searchParams.get('page')) || 1);
|
||||
let pageSize = 10;
|
||||
|
||||
let searchTransporterKey = $state($page.url.searchParams.get('transporter_key') || '');
|
||||
let searchDriverName = $state($page.url.searchParams.get('driver_name') || '');
|
||||
let searchTimeout: NodeJS.Timeout;
|
||||
@@ -31,7 +31,7 @@
|
||||
loading = true;
|
||||
try {
|
||||
const params: Record<string, string | number> = {
|
||||
page: currentPage,
|
||||
page: 1,
|
||||
page_size: pageSize
|
||||
};
|
||||
// Filtros: el backend aún no los soporta; se mantienen en URL para futura implementación
|
||||
@@ -42,8 +42,8 @@
|
||||
|
||||
if (response.data) {
|
||||
data = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
pageCount = Math.ceil(response.data.total / response.data.page_size) || 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading drivers:', error);
|
||||
@@ -52,12 +52,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
try {
|
||||
const params: Record<string, string | number> = {
|
||||
page: currentPage + 1,
|
||||
page_size: pageSize
|
||||
};
|
||||
const response = await driversApi.list(companyStore.activeCompany.id, params);
|
||||
if (response.data?.items) {
|
||||
data = [...data, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading more drivers:', error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', '1');
|
||||
if (searchTransporterKey) url.searchParams.set('transporter_key', searchTransporterKey);
|
||||
else url.searchParams.delete('transporter_key');
|
||||
if (searchDriverName) url.searchParams.set('driver_name', searchDriverName);
|
||||
@@ -74,39 +94,21 @@
|
||||
const columns = createColumns(loadData);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col space-y-6 p-8">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Conductores</h2>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Conductores</h1>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de conductores</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Conductor
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={loadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Conductor</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<Input
|
||||
placeholder="Buscar por clave transportista..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchTransporterKey}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Buscar por nombre..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchDriverName}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Conductores</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Clave transportista" class="h-9 w-44 bg-card lg:w-56" bind:value={searchTransporterKey} oninput={handleSearch} /><Input placeholder="Nombre" class="h-9 w-44 bg-card lg:w-56" bind:value={searchDriverName} oninput={handleSearch} /></div></div></Card.Header>
|
||||
<Card.Content class="p-0">{#if loading && data.length === 0}<div class="flex h-64 items-center justify-center text-muted-foreground">Cargando conductores...</div>{:else}<div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable {data} {columns} {loading} {hasMore} {loadMore} /></div>{/if}</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
{#if loading && data.length === 0}
|
||||
<div class="flex h-64 items-center justify-center text-muted-foreground">
|
||||
Cargando conductores...
|
||||
</div>
|
||||
{:else}
|
||||
<DataTable {data} {columns} {pageCount} {totalItems} />
|
||||
{/if}
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {data.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={loadData} />
|
||||
</div>
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/electronic_notices/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/electronic_notices/columns.js';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/electronic_notices/create-edit-dialog.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaAvisosElectrónicos } from '$lib/config/shortcuts/dashboard/general_catalogs/electronic_notices/list';
|
||||
import { getElectronicNotices } from '$lib/api/dashboard/a76/general_catalogs/electronic-notices';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -28,10 +32,52 @@
|
||||
let searchPedimento = $state($page.url.searchParams.get('pedimento') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.notices?.items || []);
|
||||
let currentPage = $state(data.notices?.page || 1);
|
||||
let pageSize = $state(data.notices?.page_size || 50);
|
||||
let totalItems = $state(data.notices?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.notices) {
|
||||
allItems = data.notices.items || [];
|
||||
currentPage = data.notices.page || 1;
|
||||
totalItems = data.notices.total || 0;
|
||||
pageSize = data.notices.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchNotice) filters.notice_number = searchNotice;
|
||||
if (searchPedimento) filters.pedimento = searchPedimento;
|
||||
|
||||
const response = await getElectronicNotices(
|
||||
1,
|
||||
pageSize,
|
||||
filters,
|
||||
companyStore.activeCompany.id
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = response.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchNotice) url.searchParams.set('notice_number', searchNotice);
|
||||
else url.searchParams.delete('notice_number');
|
||||
@@ -39,54 +85,92 @@
|
||||
if (searchPedimento) url.searchParams.set('pedimento', searchPedimento);
|
||||
else url.searchParams.delete('pedimento');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchNotice) filters.notice_number = searchNotice;
|
||||
if (searchPedimento) filters.pedimento = searchPedimento;
|
||||
|
||||
const response = await getElectronicNotices(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
filters,
|
||||
companyStore.activeCompany.id
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = [...allItems, ...response.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more electronic notices:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchNotice) filters.notice_number = searchNotice;
|
||||
if (searchPedimento) filters.pedimento = searchPedimento;
|
||||
|
||||
const response = await getElectronicNotices(
|
||||
1,
|
||||
pageSize,
|
||||
filters,
|
||||
companyStore.activeCompany.id
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = response.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading electronic notices:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Avisos Electrónicos</h1>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de avisos electrónicos</p>
|
||||
</div>
|
||||
<Button onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Aviso
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (dialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Aviso</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por No. Aviso..."
|
||||
bind:value={searchNotice}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por Pedimento..."
|
||||
bind:value={searchPedimento}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.notices?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.notices?.pages || 0}
|
||||
totalItems={data.notices?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Avisos</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="No. aviso" bind:value={searchNotice} oninput={handleSearch} class="h-9 w-40 bg-card lg:w-52" /><Input placeholder="Pedimento" bind:value={searchPedimento} oninput={handleSearch} class="h-9 w-40 bg-card lg:w-52" /></div></div></Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createColumns(handleSuccess)} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -4,16 +4,20 @@
|
||||
import { browser } from '$app/environment';
|
||||
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { createCatalogColumns } from '$lib/components/dashboard/general_catalogs/equivalencies/catalog-columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/equivalencies/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaEquivalencias } from '$lib/config/shortcuts/dashboard/general_catalogs/equivalencies/list';
|
||||
import DataEquivalenciesDialog from '$lib/components/dashboard/general_catalogs/equivalencies/data-equivalencies-dialog.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { getEquivalencies } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
let dataDialogOpen = $state(false);
|
||||
let selectedEquivalency = $state<Equivalency | null>(null);
|
||||
@@ -36,22 +40,115 @@
|
||||
let searchFrom = $state($page.url.searchParams.get('from_unit_code') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.equivalencies?.items || []);
|
||||
let currentPage = $state(data.equivalencies?.page || 1);
|
||||
let pageSize = $state(data.equivalencies?.page_size || 50);
|
||||
let totalItems = $state(data.equivalencies?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.equivalencies) {
|
||||
allItems = data.equivalencies.items || [];
|
||||
currentPage = data.equivalencies.page || 1;
|
||||
totalItems = data.equivalencies.total || 0;
|
||||
pageSize = data.equivalencies.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchFrom) filters.from_unit_code = searchFrom;
|
||||
|
||||
const response = await getEquivalencies(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchFrom) url.searchParams.set('from_unit_code', searchFrom);
|
||||
else url.searchParams.delete('from_unit_code');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchFrom) filters.from_unit_code = searchFrom;
|
||||
|
||||
const response = await getEquivalencies(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more equivalencies:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchFrom) filters.from_unit_code = searchFrom;
|
||||
|
||||
const response = await getEquivalencies(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading equivalencies:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
|
||||
function handleOpenInsertItems(equivalency: Equivalency) {
|
||||
@@ -67,13 +164,13 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Equivalencias</h1>
|
||||
<p class="text-muted-foreground">Catálogo de equivalencias</p>
|
||||
</div>
|
||||
<Button
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9"
|
||||
onclick={() => {
|
||||
selectedEquivalency = null;
|
||||
dataMode = 'create';
|
||||
@@ -82,29 +179,21 @@
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Equivalencia
|
||||
</Button>
|
||||
</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por código origen..."
|
||||
bind:value={searchFrom}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<DataTable
|
||||
data={data.equivalencies.items}
|
||||
columns={createCatalogColumns({
|
||||
onInsertItems: handleOpenInsertItems,
|
||||
onEdit: handleOpenEditCatalog,
|
||||
onSuccess: handleSuccess
|
||||
})}
|
||||
pageCount={data.equivalencies.pages}
|
||||
totalItems={data.equivalencies.total}
|
||||
/>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Equivalencias</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Código origen" bind:value={searchFrom} oninput={handleSearch} class="h-9 w-40 bg-card lg:w-52" /></div></div></Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createCatalogColumns({ onInsertItems: handleOpenInsertItems, onEdit: handleOpenEditCatalog, onSuccess: handleSuccess })} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<DataEquivalenciesDialog
|
||||
bind:open={dataDialogOpen}
|
||||
|
||||
@@ -3,17 +3,21 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/error_catalogs/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/error_catalogs/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/error_catalogs/create-edit-dialog.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaErrores } from '$lib/config/shortcuts/dashboard/general_catalogs/error_catalogs/list';
|
||||
import type { PageData } from './$types';
|
||||
import { getErrorCatalogs } from '$lib/api/dashboard/a76/general_catalogs/error-catalogs';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -29,10 +33,52 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.errors?.items || []);
|
||||
let currentPage = $state(data.errors?.page || 1);
|
||||
let pageSize = $state(data.errors?.page_size || 50);
|
||||
let totalItems = $state(data.errors?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.errors) {
|
||||
allItems = data.errors.items || [];
|
||||
currentPage = data.errors.page || 1;
|
||||
totalItems = data.errors.total || 0;
|
||||
pageSize = data.errors.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getErrorCatalogs(
|
||||
companyStore.activeCompany.id,
|
||||
1,
|
||||
pageSize,
|
||||
filters
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = response.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
@@ -40,50 +86,92 @@
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getErrorCatalogs(
|
||||
companyStore.activeCompany.id,
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
filters
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = [...allItems, ...response.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more error catalogs:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getErrorCatalogs(
|
||||
companyStore.activeCompany.id,
|
||||
1,
|
||||
pageSize,
|
||||
filters
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = response.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading error catalogs:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Errores de Facturación</h1>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de errores de facturación</p>
|
||||
</div>
|
||||
<Button onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Error
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (dialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Error</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.errors?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.errors?.pages || 0}
|
||||
totalItems={data.errors?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Errores</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Código" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /><Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createColumns(handleSuccess)} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -4,17 +4,21 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/exchange_rate/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/exchange_rate/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaTiposCambio } from '$lib/config/shortcuts/dashboard/general_catalogs/exchange_rate/list';
|
||||
import type { PageData } from './$types';
|
||||
import { getExchangeRates } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -36,55 +40,127 @@
|
||||
let searchCurrency = $state($page.url.searchParams.get('local_currency') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.exchange_rates?.items || []);
|
||||
let currentPage = $state(data.exchange_rates?.page || 1);
|
||||
let pageSize = $state(data.exchange_rates?.page_size || 50);
|
||||
let totalItems = $state(data.exchange_rates?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.exchange_rates) {
|
||||
allItems = data.exchange_rates.items || [];
|
||||
currentPage = data.exchange_rates.page || 1;
|
||||
totalItems = data.exchange_rates.total || 0;
|
||||
pageSize = data.exchange_rates.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await getExchangeRates(companyStore.activeCompany.id, {
|
||||
local_currency: searchCurrency || undefined,
|
||||
page: 1,
|
||||
page_size: pageSize
|
||||
});
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = response.data.page || 1;
|
||||
totalItems = response.data.total || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCurrency) url.searchParams.set('local_currency', searchCurrency);
|
||||
else url.searchParams.delete('local_currency');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await getExchangeRates(companyStore.activeCompany.id, {
|
||||
local_currency: searchCurrency || undefined,
|
||||
page: currentPage + 1,
|
||||
page_size: pageSize
|
||||
});
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more exchange rates:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await getExchangeRates(companyStore.activeCompany.id, {
|
||||
local_currency: searchCurrency || undefined,
|
||||
page: 1,
|
||||
page_size: pageSize
|
||||
});
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = response.data.page || 1;
|
||||
totalItems = response.data.total || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading exchange rates:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Tipos de Cambio</h1>
|
||||
<p class="text-muted-foreground">Catálogo de tipos de cambio</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Tipo de Cambio
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Tipo de Cambio</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por moneda..."
|
||||
bind:value={searchCurrency}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.exchange_rates?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.exchange_rates?.pages || 0}
|
||||
totalItems={data.exchange_rates?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Tipos de Cambio</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Moneda" bind:value={searchCurrency} oninput={handleSearch} class="h-9 w-40 bg-card lg:w-52" /></div></div></Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createColumns(handleSuccess)} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -4,17 +4,21 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/identifiers/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/identifiers/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/identifiers/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaIdentificadores } from '$lib/config/shortcuts/dashboard/general_catalogs/identifiers/list';
|
||||
import { getIdentifiers } from '$lib/api/dashboard/a76/general_catalogs/identifiers';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -30,10 +34,47 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.identifiers?.items || []);
|
||||
let currentPage = $state(data.identifiers?.page || 1);
|
||||
let pageSize = $state(data.identifiers?.page_size || 50);
|
||||
let totalItems = $state(data.identifiers?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.identifiers) {
|
||||
allItems = data.identifiers.items || [];
|
||||
currentPage = data.identifiers.page || 1;
|
||||
totalItems = data.identifiers.total || 0;
|
||||
pageSize = data.identifiers.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getIdentifiers(1, pageSize, companyStore.activeCompany.id, filters);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
@@ -41,49 +82,125 @@
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getIdentifiers(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more identifiers:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getIdentifiers(1, pageSize, companyStore.activeCompany.id, filters);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading identifiers:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Identificadores</h1>
|
||||
<p class="text-muted-foreground">Catálogo de identificadores</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Identificador
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por clave..." bind:value={searchCode} oninput={handleSearch} />
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button class="h-9" onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Identificador
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.identifiers.items}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.identifiers.pages}
|
||||
totalItems={data.identifiers.total}
|
||||
/>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Identificadores</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Clave"
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-36 bg-card lg:w-44"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Descripción"
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="p-0">
|
||||
<div class="rounded-md border bg-background overflow-hidden">
|
||||
<InfiniteDataTable
|
||||
data={allItems}
|
||||
columns={createColumns(handleSuccess)}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
|
||||
|
||||
@@ -4,17 +4,21 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/inpc/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/inpc/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/inpc/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaInpc } from '$lib/config/shortcuts/dashboard/general_catalogs/inpc/list';
|
||||
import { getINPCs } from '$lib/api/dashboard/a76/general_catalogs/inpc';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -30,10 +34,48 @@
|
||||
let searchMonth = $state($page.url.searchParams.get('month') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
// Infinite scroll
|
||||
let allItems = $state(data.inpc?.items || []);
|
||||
let currentPage = $state(data.inpc?.page || 1);
|
||||
let pageSize = $state(data.inpc?.page_size || 50);
|
||||
let totalItems = $state(data.inpc?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.inpc) {
|
||||
allItems = data.inpc.items || [];
|
||||
currentPage = data.inpc.page || 1;
|
||||
totalItems = data.inpc.total || 0;
|
||||
pageSize = data.inpc.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchYear) filters.year = searchYear;
|
||||
if (searchMonth) filters.month = searchMonth;
|
||||
|
||||
const response = await getINPCs(1, pageSize, companyStore.activeCompany.id, filters);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchYear) url.searchParams.set('year', searchYear);
|
||||
else url.searchParams.delete('year');
|
||||
@@ -41,45 +83,125 @@
|
||||
if (searchMonth) url.searchParams.set('month', searchMonth);
|
||||
else url.searchParams.delete('month');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchYear) filters.year = searchYear;
|
||||
if (searchMonth) filters.month = searchMonth;
|
||||
|
||||
const response = await getINPCs(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more INPC:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchYear) filters.year = searchYear;
|
||||
if (searchMonth) filters.month = searchMonth;
|
||||
|
||||
const response = await getINPCs(1, pageSize, companyStore.activeCompany.id, filters);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading INPC:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">INPC</h1>
|
||||
<p class="text-muted-foreground">Índice Nacional de Precios al Consumidor</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo INPC
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por año..." bind:value={searchYear} oninput={handleSearch} />
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por mes..." bind:value={searchMonth} oninput={handleSearch} />
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo INPC
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.inpc.items}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.inpc.pages}
|
||||
totalItems={data.inpc.total}
|
||||
/>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex flex-1 min-h-0 flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de INPC</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Año"
|
||||
bind:value={searchYear}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-28 bg-card lg:w-36"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Mes"
|
||||
bind:value={searchMonth}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-28 bg-card lg:w-36"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 flex-1 p-0">
|
||||
<div class="flex h-full min-h-0 rounded-md border bg-background overflow-hidden flex-col">
|
||||
<InfiniteDataTable
|
||||
data={allItems}
|
||||
columns={createColumns(handleSuccess)}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/legends/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/legends/columns';
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/legends/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaLeyendas } from '$lib/config/shortcuts/dashboard/general_catalogs/legends/list';
|
||||
import { getLegends } from '$lib/api/dashboard/a76/general_catalogs/legends';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -28,17 +32,49 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
|
||||
const columns = $derived(createColumns(handleSuccess));
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.legends?.items || []);
|
||||
let currentPage = $state(data.legends?.page || 1);
|
||||
let pageSize = $state(data.legends?.page_size || 50);
|
||||
let totalItems = $state(data.legends?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.legends) {
|
||||
allItems = data.legends.items || [];
|
||||
currentPage = data.legends.page || 1;
|
||||
totalItems = data.legends.total || 0;
|
||||
pageSize = data.legends.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getLegends(1, pageSize, companyStore.activeCompany.id, filters);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
@@ -46,44 +82,125 @@
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getLegends(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more legends:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getLegends(1, pageSize, companyStore.activeCompany.id, filters);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading legends:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Leyendas Fijas</h1>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de leyendas fijas</p>
|
||||
</div>
|
||||
<Button onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Leyenda
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button class="h-9" onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Leyenda
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.legends?.items || []}
|
||||
{columns}
|
||||
pageCount={data.legends?.pages || 0}
|
||||
totalItems={data.legends?.total || 0}
|
||||
/>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex flex-1 min-h-0 flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Leyendas</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Código"
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-36 bg-card lg:w-44"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Descripción"
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 flex-1 p-0">
|
||||
<div class="flex h-full min-h-0 rounded-md border bg-background overflow-hidden flex-col">
|
||||
<InfiniteDataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
|
||||
<CreateDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/multi_currency_types/create-edit-dialog.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaMonedas } from '$lib/config/shortcuts/dashboard/general_catalogs/multi_currency_types/list';
|
||||
import {
|
||||
getMultiCurrencyTypes,
|
||||
type MultiCurrencyType
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/multi-currency-types';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -27,22 +35,74 @@
|
||||
let searchCountry = $state($page.url.searchParams.get('country_key') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = [
|
||||
{ key: 'currency_type_code', label: 'Código Moneda' },
|
||||
{ key: 'country_key', label: 'País' },
|
||||
{ key: 'conversion_factor', label: 'Factor Conversión' },
|
||||
{ key: 'publication_date', label: 'Fecha Publicación' }
|
||||
const columns: ColumnDef<MultiCurrencyType>[] = [
|
||||
{
|
||||
accessorKey: 'currency_type_code',
|
||||
header: 'Código Moneda',
|
||||
cell: ({ row }) => row.original.currency_type_code || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'country_key',
|
||||
header: 'País',
|
||||
cell: ({ row }) => row.original.country_key || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'conversion_factor',
|
||||
header: 'Factor Conversión',
|
||||
cell: ({ row }) => row.original.conversion_factor ?? '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'publication_date',
|
||||
header: 'Fecha Publicación',
|
||||
cell: ({ row }) => row.original.publication_date || '-'
|
||||
}
|
||||
];
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
let allItems = $state(data.types?.items || []);
|
||||
let currentPage = $state(data.types?.page || 1);
|
||||
let pageSize = $state(data.types?.page_size || 50);
|
||||
let totalItems = $state(data.types?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
function handleSearch() {
|
||||
$effect(() => {
|
||||
if (data.types) {
|
||||
allItems = data.types.items || [];
|
||||
currentPage = data.types.page || 1;
|
||||
totalItems = data.types.total || 0;
|
||||
pageSize = data.types.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await getMultiCurrencyTypes(
|
||||
companyStore.activeCompany.id,
|
||||
1,
|
||||
pageSize,
|
||||
{
|
||||
currency_type_code: searchCode || undefined,
|
||||
country_key: searchCountry || undefined
|
||||
}
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = response.data.items;
|
||||
currentPage = response.data.page || 1;
|
||||
totalItems = response.data.total || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('currency_type_code', searchCode);
|
||||
else url.searchParams.delete('currency_type_code');
|
||||
@@ -50,41 +110,90 @@
|
||||
if (searchCountry) url.searchParams.set('country_key', searchCountry);
|
||||
else url.searchParams.delete('country_key');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await getMultiCurrencyTypes(
|
||||
companyStore.activeCompany.id,
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
{
|
||||
currency_type_code: searchCode || undefined,
|
||||
country_key: searchCountry || undefined
|
||||
}
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more multi-currency types:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await getMultiCurrencyTypes(
|
||||
companyStore.activeCompany.id,
|
||||
1,
|
||||
pageSize,
|
||||
{
|
||||
currency_type_code: searchCode || undefined,
|
||||
country_key: searchCountry || undefined
|
||||
}
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = response.data.items;
|
||||
currentPage = response.data.page || 1;
|
||||
totalItems = response.data.total || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading multi-currency types:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Tipos de Moneda Múltiple</h1>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de tipos de moneda múltiple</p>
|
||||
</div>
|
||||
<Button onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Tipo
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (dialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Tipo</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por país..." bind:value={searchCountry} oninput={handleSearch} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border">
|
||||
<SimpleDataTable
|
||||
data={data.types?.items || []}
|
||||
{columns}
|
||||
pageCount={data.types?.pages || 0}
|
||||
totalItems={data.types?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Monedas</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Código" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /><Input placeholder="País" bind:value={searchCountry} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /></div></div></Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import DataTable from '$lib/components/dashboard/packages/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/packages/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/packages/create-edit-dialog.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaBultos } from '$lib/config/shortcuts/dashboard/general_catalogs/packages/list';
|
||||
import { getPackages } from '$lib/api/dashboard/a76/general_catalogs/packages';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Filtros
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
@@ -31,62 +35,173 @@
|
||||
})
|
||||
);
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.packages?.items || []);
|
||||
let currentPage = $state(data.packages?.page || 1);
|
||||
let pageSize = $state(data.packages?.page_size || 50);
|
||||
let totalItems = $state(data.packages?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.packages) {
|
||||
allItems = data.packages.items || [];
|
||||
currentPage = data.packages.page || 1;
|
||||
totalItems = data.packages.total || 0;
|
||||
pageSize = data.packages.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchName) filters.name = searchName;
|
||||
|
||||
const response = await getPackages(1, pageSize, companyStore.activeCompany.id, filters);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
|
||||
if (searchName) url.searchParams.set('name', searchName);
|
||||
else url.searchParams.delete('name');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchName) filters.name = searchName;
|
||||
|
||||
const response = await getPackages(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more packages:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchName) filters.name = searchName;
|
||||
|
||||
const response = await getPackages(1, pageSize, companyStore.activeCompany.id, filters);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading packages:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
// Recargar datos
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Bultos y Embalajes</h1>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de bultos y embalajes</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Bulto
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por clave..." bind:value={searchCode} oninput={handleSearch} />
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchName}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button class="h-9" onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Bulto
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.packages?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.packages?.pages || 0}
|
||||
totalItems={data.packages?.total || 0}
|
||||
/>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Bultos</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Clave"
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-36 bg-card lg:w-44"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Descripción"
|
||||
bind:value={searchName}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="p-0">
|
||||
<div class="rounded-md border bg-background overflow-hidden">
|
||||
<InfiniteDataTable
|
||||
data={allItems}
|
||||
columns={createColumns(handleSuccess)}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
|
||||
|
||||
@@ -4,17 +4,21 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/ports/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/ports/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaPuertos } from '$lib/config/shortcuts/dashboard/general_catalogs/ports/list';
|
||||
import { portsApi } from '$lib/api/dashboard/a76/general_catalogs/ports';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.items?.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -30,10 +34,50 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.items?.items || data.items || []);
|
||||
let currentPage = $state(data.items?.page || 1);
|
||||
let pageSize = $state(data.items?.pageSize || data.items?.page_size || 50);
|
||||
let totalItems = $state(data.items?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.items?.items) {
|
||||
allItems = data.items.items || [];
|
||||
currentPage = data.items.page || 1;
|
||||
totalItems = data.items.total || 0;
|
||||
pageSize = data.items.pageSize || data.items.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
page: '1',
|
||||
page_size: pageSize.toString()
|
||||
};
|
||||
if (searchCode) params.port_code = searchCode;
|
||||
if (searchDesc) params.description = searchDesc;
|
||||
|
||||
const response = await portsApi.list(companyStore.activeCompany.id, params);
|
||||
if (response.data?.items) {
|
||||
allItems = response.data.items;
|
||||
currentPage = response.data.page || 1;
|
||||
totalItems = response.data.total || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('port_code', searchCode);
|
||||
else url.searchParams.delete('port_code');
|
||||
@@ -41,58 +85,117 @@
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
page: (currentPage + 1).toString(),
|
||||
page_size: pageSize.toString()
|
||||
};
|
||||
if (searchCode) params.port_code = searchCode;
|
||||
if (searchDesc) params.description = searchDesc;
|
||||
|
||||
const response = await portsApi.list(companyStore.activeCompany.id, params);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more ports:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
page: '1',
|
||||
page_size: pageSize.toString()
|
||||
};
|
||||
if (searchCode) params.port_code = searchCode;
|
||||
if (searchDesc) params.description = searchDesc;
|
||||
|
||||
const response = await portsApi.list(companyStore.activeCompany.id, params);
|
||||
if (response.data?.items) {
|
||||
allItems = response.data.items;
|
||||
currentPage = response.data.page || 1;
|
||||
totalItems = response.data.total || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading ports:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Puertos</h1>
|
||||
<p class="text-muted-foreground">Catálogo de puertos</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Puerto
|
||||
</Button>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button class="h-9" onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Puerto
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if data.items?.error}
|
||||
{#if error}
|
||||
<div
|
||||
class="rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"
|
||||
>
|
||||
{data.items.error}
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Puertos</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Código" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" />
|
||||
<Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="p-0">
|
||||
<div class="rounded-md border bg-background overflow-hidden">
|
||||
<InfiniteDataTable
|
||||
data={allItems}
|
||||
columns={createColumns(handleSuccess)}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.items?.items || data.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.items?.pages || data.items?.pageCount || 0}
|
||||
totalItems={data.items?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/prevalidators/data-table.svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/prevalidators/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/prevalidators/create-edit-dialog.svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaPrevalidadores } from '$lib/config/shortcuts/dashboard/general_catalogs/prevalidators/list';
|
||||
import { getPrevalidators } from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -27,15 +31,47 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
let allItems = $state(data.prevalidators?.items || []);
|
||||
let currentPage = $state(data.prevalidators?.page || 1);
|
||||
let pageSize = $state(data.prevalidators?.page_size || 50);
|
||||
let totalItems = $state(data.prevalidators?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
function handleSearch() {
|
||||
$effect(() => {
|
||||
if (data.prevalidators) {
|
||||
allItems = data.prevalidators.items || [];
|
||||
currentPage = data.prevalidators.page || 1;
|
||||
totalItems = data.prevalidators.total || 0;
|
||||
pageSize = data.prevalidators.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getPrevalidators(1, pageSize, filters, companyStore.activeCompany.id);
|
||||
if (response?.items) {
|
||||
allItems = response.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
@@ -43,45 +79,98 @@
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getPrevalidators(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
filters,
|
||||
companyStore.activeCompany.id
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = [...allItems, ...response.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more prevalidators:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getPrevalidators(1, pageSize, filters, companyStore.activeCompany.id);
|
||||
if (response?.items) {
|
||||
allItems = response.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading prevalidators:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Prevalidadores</h1>
|
||||
<p class="text-muted-foreground">Catálogo de prevalidadores</p>
|
||||
</div>
|
||||
<Button onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Prevalidador
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button>
|
||||
<Button class="h-9" onclick={() => (dialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Prevalidador</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.prevalidators?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.prevalidators?.pages || 0}
|
||||
totalItems={data.prevalidators?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Prevalidadores</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Código" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" />
|
||||
<Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createColumns(handleSuccess)} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import DataTable from '$lib/components/dashboard/seal/data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/seal/create-edit-dialog.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/seal/columns';
|
||||
@@ -17,6 +18,7 @@
|
||||
let error = $state<string | null>(null);
|
||||
let currentPage = $state(1);
|
||||
const pageSize = 50;
|
||||
let totalItems = $state(0);
|
||||
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
@@ -104,6 +106,7 @@
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
}
|
||||
|
||||
totalItems = response.data.total ?? allItems.length;
|
||||
currentPage = page;
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al cargar los sellos';
|
||||
@@ -130,40 +133,61 @@
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Sellos</h1>
|
||||
<p class="text-muted-foreground">Gestiona los sellos de tu empresa</p>
|
||||
</div>
|
||||
<Button onclick={handleCreate}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Sello
|
||||
</Button>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button class="h-9" onclick={handleCreate}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Sello
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-4">
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
<p class="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Filtrar por sello..."
|
||||
bind:value={sealFilter}
|
||||
oninput={handleSearch}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-1 min-h-0 flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Sellos</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Filtrar por sello"
|
||||
bind:value={sealFilter}
|
||||
oninput={handleSearch}
|
||||
disabled={loading}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 flex-1 p-0">
|
||||
<div class="flex h-full min-h-0 rounded-md border bg-background overflow-hidden flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems || allItems.length} registros
|
||||
</div>
|
||||
|
||||
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
|
||||
|
||||
@@ -4,15 +4,19 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/signatures/data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/signatures/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/signatures/create-edit-dialog.svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaFirmas } from '$lib/config/shortcuts/dashboard/general_catalogs/signatures/list';
|
||||
import { getSignatures } from '$lib/api/dashboard/a76/general_catalogs/signatures';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -26,50 +30,138 @@
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
let allItems = $state(data.signatures?.items || []);
|
||||
let currentPage = $state(data.signatures?.page || 1);
|
||||
let pageSize = $state(data.signatures?.page_size || 50);
|
||||
let totalItems = $state(data.signatures?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
function handleSearch() {
|
||||
$effect(() => {
|
||||
if (data.signatures) {
|
||||
allItems = data.signatures.items || [];
|
||||
currentPage = data.signatures.page || 1;
|
||||
totalItems = data.signatures.total || 0;
|
||||
pageSize = data.signatures.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
|
||||
const response = await getSignatures(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = response.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
|
||||
const response = await getSignatures(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = [...allItems, ...response.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more signatures:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
|
||||
const response = await getSignatures(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = response.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading signatures:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Firmas Electrónicas</h1>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de firmas electrónicas</p>
|
||||
</div>
|
||||
<Button onclick={() => (dialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Firma
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (dialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nueva Firma</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.signatures?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.signatures?.pages || 0}
|
||||
totalItems={data.signatures?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Firmas</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Código" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /></div></div></Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createColumns(handleSuccess)} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -89,145 +89,100 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-6">
|
||||
<Card.Root>
|
||||
<Card.Header class="border-b text-center">
|
||||
<Card.Title class="text-2xl font-bold uppercase">
|
||||
Catálogo de Fracciones SITAR - SCAII
|
||||
</Card.Title>
|
||||
<p class="mt-2 text-muted-foreground">Nomenclatura arancelaria mexicana completa</p>
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Catálogo de Fracciones SITAR - SCAII</h1>
|
||||
<p class="text-muted-foreground">Nomenclatura arancelaria mexicana completa</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" class="h-9" title="Exportar a CSV">
|
||||
<Download class="mr-2 h-4 w-4" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card.Root class="flex min-h-0 flex-1 flex-col border bg-background">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Fracciones</Card.Title>
|
||||
<div class="relative min-w-[320px] flex-1 xl:max-w-xl">
|
||||
<Search class="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
bind:value={searchQuery}
|
||||
placeholder="Buscar por código, fracción, descripción, NICO o UMT..."
|
||||
class="h-9 bg-card pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content class="pt-6">
|
||||
<div class="space-y-6">
|
||||
<!-- Barra de búsqueda y acciones -->
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative flex-1">
|
||||
<Search
|
||||
class="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<Input
|
||||
bind:value={searchQuery}
|
||||
placeholder="Buscar por código, fracción, descripción, NICO o UMT (Búsqueda en servidor)..."
|
||||
class="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" title="Exportar a CSV">
|
||||
<Download class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Información de registros -->
|
||||
<div class="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<div>
|
||||
{#if isLoading}
|
||||
<div class="flex items-center gap-2">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
<span>Cargando...</span>
|
||||
</div>
|
||||
{:else}
|
||||
Mostrando {tariffFractions.length} de {totalRecords} fracciones arancelarias
|
||||
{#if searchQuery}
|
||||
(filtrado)
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if totalPages > 1}
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Página {currentPage} de {totalPages}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Tabla de fracciones -->
|
||||
<div class="max-h-[600px] overflow-auto rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header class="z-10 bg-background">
|
||||
<Card.Content class="min-h-0 p-0">
|
||||
<div class="catalog-table-shell bg-background">
|
||||
<Table.Root>
|
||||
<Table.Header class="catalog-table-header">
|
||||
<Table.Row>
|
||||
<Table.Head class="catalog-table-head-cell w-[100px]">Código</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell w-[120px]">Fracción</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell min-w-[350px]">Descripción</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell w-[80px]">NICO</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell w-[80px]">UMT</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell w-[100px]">Adv. Impo</Table.Head>
|
||||
<Table.Head class="catalog-table-head-cell w-[100px]">Adv. Expo</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if tariffFractions.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Head class="w-[100px]">Código</Table.Head>
|
||||
<Table.Head class="w-[120px]">Fracción</Table.Head>
|
||||
<Table.Head class="min-w-[350px]">Descripción</Table.Head>
|
||||
<Table.Head class="w-[80px]">NICO</Table.Head>
|
||||
<Table.Head class="w-[80px]">UMT</Table.Head>
|
||||
<Table.Head class="w-[100px]">Adv. Impo</Table.Head>
|
||||
<Table.Head class="w-[100px]">Adv. Expo</Table.Head>
|
||||
<Table.Cell colspan={7} class="h-24 text-center text-sm text-muted-foreground">
|
||||
{#if isLoading}
|
||||
Cargando fracciones arancelarias...
|
||||
{:else if searchQuery}
|
||||
No se encontraron fracciones que coincidan con la búsqueda
|
||||
{:else}
|
||||
No hay fracciones arancelarias disponibles
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if tariffFractions.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={7} class="py-8 text-center text-muted-foreground">
|
||||
{#if isLoading}
|
||||
Cargando fracciones arancelarias...
|
||||
{:else if searchQuery}
|
||||
No se encontraron fracciones que coincidan con la búsqueda
|
||||
{:else}
|
||||
No hay fracciones arancelarias disponibles
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{:else}
|
||||
{#each tariffFractions as fraction (fraction.id)}
|
||||
<Table.Row class="catalog-table-row">
|
||||
<Table.Cell class="font-mono text-sm">{fraction.code}</Table.Cell>
|
||||
<Table.Cell class="font-mono text-sm font-medium">{fraction.fraction}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{fraction.description || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{fraction.nico || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{fraction.umt || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{fraction.adv_impo || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{fraction.adv_expo || '-'}</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each tariffFractions as fraction (fraction.id)}
|
||||
<Table.Row class="hover:bg-muted/50">
|
||||
<Table.Cell class="font-mono text-sm">{fraction.code}</Table.Cell>
|
||||
<Table.Cell class="font-mono text-sm font-medium"
|
||||
>{fraction.fraction}</Table.Cell
|
||||
>
|
||||
<Table.Cell class="text-sm">
|
||||
{fraction.description || '-'}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-sm">{fraction.nico || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{fraction.umt || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{fraction.adv_impo || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{fraction.adv_expo || '-'}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<!-- Paginación -->
|
||||
{#if totalPages > 1}
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage === 1 || isLoading}
|
||||
onclick={() => goToPage(1)}
|
||||
>
|
||||
Primera
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage === 1 || isLoading}
|
||||
onclick={() => goToPage(currentPage - 1)}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<span class="px-4 text-sm">
|
||||
Página {currentPage} de {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage === totalPages || isLoading}
|
||||
onclick={() => goToPage(currentPage + 1)}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage === totalPages || isLoading}
|
||||
onclick={() => goToPage(totalPages)}
|
||||
>
|
||||
Última
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {tariffFractions.length} de {totalRecords} fracciones arancelarias
|
||||
{#if searchQuery}
|
||||
<span class="ml-2">•</span>
|
||||
<span class="ml-2">Filtrado</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if totalPages > 1}
|
||||
<div class="flex-none flex items-center justify-center gap-2">
|
||||
<Button variant="outline" size="sm" class="h-9" disabled={currentPage === 1 || isLoading} onclick={() => goToPage(1)}>
|
||||
Primera
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" class="h-9" disabled={currentPage === 1 || isLoading} onclick={() => goToPage(currentPage - 1)}>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" class="h-9" disabled={currentPage === totalPages || isLoading} onclick={() => goToPage(currentPage + 1)}>
|
||||
Siguiente
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" class="h-9" disabled={currentPage === totalPages || isLoading} onclick={() => goToPage(totalPages)}>
|
||||
Última
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold tracking-tight">{m['sidebar.fractions.canadian']()}</h1>
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">{m['sidebar.fractions.canadian']()}</h1>
|
||||
<p class="text-muted-foreground">Catálogo de fracciones arancelarias canadienses</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-hidden flex flex-col">
|
||||
<CanadianFractionList />
|
||||
</div>
|
||||
<CanadianFractionList />
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
|
||||
// Importar componentes de la librería
|
||||
import DataTable from '$lib/components/dashboard/transportation/trailers/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/transportation/trailers/create-edit-dialog.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/transportation/trailers/columns';
|
||||
|
||||
@@ -17,13 +18,12 @@
|
||||
// --- ESTADO ---
|
||||
let data = $state<Trailer[]>([]);
|
||||
let totalItems = $state(0);
|
||||
let pageCount = $state(0);
|
||||
let loading = $state(false);
|
||||
let currentPage = $state(1);
|
||||
let pageSize = $state(50);
|
||||
let hasMore = $derived(data.length < totalItems);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
let currentPage = $derived(Number($page.url.searchParams.get('page')) || 1);
|
||||
let pageSize = 10;
|
||||
|
||||
// Filtros
|
||||
let searchNumber = $state($page.url.searchParams.get('trailer_number') || '');
|
||||
let searchPlate = $state($page.url.searchParams.get('plate_number') || '');
|
||||
@@ -35,7 +35,7 @@
|
||||
loading = true;
|
||||
try {
|
||||
const response = await trailersApi.list(companyStore.activeCompany.id, {
|
||||
page: currentPage,
|
||||
page: 1,
|
||||
page_size: pageSize,
|
||||
trailer_number: searchNumber,
|
||||
plate_number: searchPlate
|
||||
@@ -43,8 +43,8 @@
|
||||
|
||||
if (response.data) {
|
||||
data = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
pageCount = Math.ceil(response.data.total / response.data.page_size);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading trailers:', error);
|
||||
@@ -53,12 +53,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
try {
|
||||
const response = await trailersApi.list(companyStore.activeCompany.id, {
|
||||
page: currentPage + 1,
|
||||
page_size: pageSize,
|
||||
trailer_number: searchNumber,
|
||||
plate_number: searchPlate
|
||||
});
|
||||
|
||||
if (response.data?.items) {
|
||||
data = [...data, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading more trailers:', error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', '1');
|
||||
|
||||
if (searchNumber) url.searchParams.set('trailer_number', searchNumber);
|
||||
else url.searchParams.delete('trailer_number');
|
||||
@@ -79,39 +101,21 @@
|
||||
const columns = createColumns(loadData);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col space-y-6 p-8">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Trailers</h2>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Trailers</h1>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de trailers de la compañía</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Trailer
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={loadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Trailer</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<Input
|
||||
placeholder="Buscar por número..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchNumber}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Buscar por placas..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchPlate}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Trailers</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Número" class="h-9 w-40 bg-card lg:w-52" bind:value={searchNumber} oninput={handleSearch} /><Input placeholder="Placas" class="h-9 w-40 bg-card lg:w-52" bind:value={searchPlate} oninput={handleSearch} /></div></div></Card.Header>
|
||||
<Card.Content class="p-0">{#if loading && data.length === 0}<div class="flex h-64 items-center justify-center text-muted-foreground">Cargando trailers...</div>{:else}<div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable {data} {columns} {loading} {hasMore} {loadMore} /></div>{/if}</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
{#if loading && data.length === 0}
|
||||
<div class="flex h-64 items-center justify-center text-muted-foreground">
|
||||
Cargando trailers...
|
||||
</div>
|
||||
{:else}
|
||||
<DataTable {data} {columns} {pageCount} {totalItems} />
|
||||
{/if}
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {data.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={loadData} />
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
|
||||
// Importar componentes de la librería
|
||||
import DataTable from '$lib/components/dashboard/transportation/transporters/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/transportation/transporters/transporter-columns';
|
||||
|
||||
@@ -17,13 +18,12 @@
|
||||
// --- ESTADO ---
|
||||
let data = $state<Transporter[]>([]);
|
||||
let totalItems = $state(0);
|
||||
let pageCount = $state(0);
|
||||
let loading = $state(false);
|
||||
let currentPage = $state(1);
|
||||
let pageSize = $state(50);
|
||||
let hasMore = $derived(data.length < totalItems);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
let currentPage = $derived(Number($page.url.searchParams.get('page')) || 1);
|
||||
let pageSize = 10;
|
||||
|
||||
// Filtros
|
||||
let searchKey = $state($page.url.searchParams.get('transporter_key') || '');
|
||||
let searchName = $state($page.url.searchParams.get('name') || '');
|
||||
@@ -35,7 +35,7 @@
|
||||
loading = true;
|
||||
try {
|
||||
const response = await transportersApi.list(companyStore.activeCompany.id, {
|
||||
page: currentPage,
|
||||
page: 1,
|
||||
page_size: pageSize,
|
||||
transporter_key: searchKey,
|
||||
name: searchName
|
||||
@@ -43,8 +43,8 @@
|
||||
|
||||
if (response.data) {
|
||||
data = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
pageCount = Math.ceil(response.data.total / response.data.page_size);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading transporters:', error);
|
||||
@@ -53,12 +53,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
try {
|
||||
const response = await transportersApi.list(companyStore.activeCompany.id, {
|
||||
page: currentPage + 1,
|
||||
page_size: pageSize,
|
||||
transporter_key: searchKey,
|
||||
name: searchName
|
||||
});
|
||||
|
||||
if (response.data?.items) {
|
||||
data = [...data, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading more transporters:', error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', '1');
|
||||
|
||||
if (searchKey) url.searchParams.set('transporter_key', searchKey);
|
||||
else url.searchParams.delete('transporter_key');
|
||||
@@ -79,39 +101,21 @@
|
||||
const columns = createColumns(loadData);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col space-y-6 p-8">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Transportistas</h2>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Transportistas</h1>
|
||||
<p class="text-muted-foreground">Gestión del catálogo de líneas transportistas</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Transportista
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={loadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Transportista</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<Input
|
||||
placeholder="Buscar por clave..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchKey}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Buscar por nombre..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchName}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Transportistas</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Clave" class="h-9 w-40 bg-card lg:w-52" bind:value={searchKey} oninput={handleSearch} /><Input placeholder="Nombre" class="h-9 w-44 bg-card lg:w-56" bind:value={searchName} oninput={handleSearch} /></div></div></Card.Header>
|
||||
<Card.Content class="p-0">{#if loading && data.length === 0}<div class="flex h-64 items-center justify-center text-muted-foreground">Cargando transportistas...</div>{:else}<div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable {data} {columns} {loading} {hasMore} {loadMore} /></div>{/if}</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
{#if loading && data.length === 0}
|
||||
<div class="flex h-64 items-center justify-center text-muted-foreground">
|
||||
Cargando transportistas...
|
||||
</div>
|
||||
{:else}
|
||||
<DataTable {data} {columns} {pageCount} {totalItems} />
|
||||
{/if}
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {data.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={loadData} />
|
||||
</div>
|
||||
|
||||
@@ -4,17 +4,21 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/unit_conversions/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/unit_conversions/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/unit_conversions/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaConversiones } from '$lib/config/shortcuts/dashboard/general_catalogs/unit_conversions/list';
|
||||
import { getUnitConversions } from '$lib/api/dashboard/a76/general_catalogs/unit-conversions';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -30,10 +34,52 @@
|
||||
let searchTo = $state($page.url.searchParams.get('to_unit_code') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.conversions?.items || []);
|
||||
let currentPage = $state(data.conversions?.page || 1);
|
||||
let pageSize = $state(data.conversions?.page_size || 50);
|
||||
let totalItems = $state(data.conversions?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.conversions) {
|
||||
allItems = data.conversions.items || [];
|
||||
currentPage = data.conversions.page || 1;
|
||||
totalItems = data.conversions.total || 0;
|
||||
pageSize = data.conversions.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchFrom) filters.from_unit_code = searchFrom;
|
||||
if (searchTo) filters.to_unit_code = searchTo;
|
||||
|
||||
const response = await getUnitConversions(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = response.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchFrom) url.searchParams.set('from_unit_code', searchFrom);
|
||||
else url.searchParams.delete('from_unit_code');
|
||||
@@ -41,46 +87,92 @@
|
||||
if (searchTo) url.searchParams.set('to_unit_code', searchTo);
|
||||
else url.searchParams.delete('to_unit_code');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchFrom) filters.from_unit_code = searchFrom;
|
||||
if (searchTo) filters.to_unit_code = searchTo;
|
||||
|
||||
const response = await getUnitConversions(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = [...allItems, ...response.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more unit conversions:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchFrom) filters.from_unit_code = searchFrom;
|
||||
if (searchTo) filters.to_unit_code = searchTo;
|
||||
|
||||
const response = await getUnitConversions(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response?.items) {
|
||||
allItems = response.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading unit conversions:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Conversiones de Unidades</h1>
|
||||
<p class="text-muted-foreground">Catálogo de conversiones de unidades de medida</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Conversión
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nueva Conversión</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar desde código..." bind:value={searchFrom} oninput={handleSearch} />
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar hacia código..." bind:value={searchTo} oninput={handleSearch} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.conversions?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.conversions?.pages || 0}
|
||||
totalItems={data.conversions?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Conversiones</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Desde" bind:value={searchFrom} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /><Input placeholder="Hacia" bind:value={searchTo} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /></div></div></Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createColumns(handleSuccess)} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -4,17 +4,21 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaUnidadesACE } from '$lib/config/shortcuts/dashboard/general_catalogs/units_of_measure/ace/list';
|
||||
import { getUnitsOfMeasureACE } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -30,10 +34,52 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.ace_units?.items || []);
|
||||
let currentPage = $state(data.ace_units?.page || 1);
|
||||
let pageSize = $state(data.ace_units?.page_size || 50);
|
||||
let totalItems = $state(data.ace_units?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.ace_units) {
|
||||
allItems = data.ace_units.items || [];
|
||||
currentPage = data.ace_units.page || 1;
|
||||
totalItems = data.ace_units.total || 0;
|
||||
pageSize = data.ace_units.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureACE(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
@@ -41,50 +87,92 @@
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureACE(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more ACE units:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureACE(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading ACE units:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Unidades de Medida ACE</h1>
|
||||
<p class="text-muted-foreground">Catálogo de unidades de medida ACE</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Unidad
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nueva Unidad</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por clave..." bind:value={searchCode} oninput={handleSearch} />
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.ace_units?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.ace_units?.pages || 0}
|
||||
totalItems={data.ace_units?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Unidades ACE</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Clave" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /><Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable data={allItems} columns={createColumns(handleSuccess)} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/american/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/american/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/american/create-edit-dialog.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaUnidadesAmericanas } from '$lib/config/shortcuts/dashboard/general_catalogs/units_of_measure/american/list';
|
||||
import { getUnitsOfMeasureAmerican } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -30,10 +34,52 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.american_units?.items || []);
|
||||
let currentPage = $state(data.american_units?.page || 1);
|
||||
let pageSize = $state(data.american_units?.page_size || 50);
|
||||
let totalItems = $state(data.american_units?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.american_units) {
|
||||
allItems = data.american_units.items || [];
|
||||
currentPage = data.american_units.page || 1;
|
||||
totalItems = data.american_units.total || 0;
|
||||
pageSize = data.american_units.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureAmerican(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
@@ -41,50 +87,92 @@
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureAmerican(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more American units:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureAmerican(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading American units:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Unidades de Medida Americanas</h1>
|
||||
<p class="text-muted-foreground">Catálogo de unidades de medida Americanas</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Unidad
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nueva Unidad</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por clave..." bind:value={searchCode} oninput={handleSearch} />
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
columns={createColumns(handleSuccess)}
|
||||
data={data.american_units?.items || []}
|
||||
pageCount={data.american_units?.pages || 0}
|
||||
totalItems={data.american_units?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Unidades Americanas</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Clave" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /><Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable columns={createColumns(handleSuccess)} data={allItems} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/customs/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/customs/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/customs/create-edit-dialog.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaUnidadesAduanas } from '$lib/config/shortcuts/dashboard/general_catalogs/units_of_measure/customs/list';
|
||||
import { getUnitsOfMeasureCustoms } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -30,10 +34,52 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.customs_units?.items || []);
|
||||
let currentPage = $state(data.customs_units?.page || 1);
|
||||
let pageSize = $state(data.customs_units?.page_size || 50);
|
||||
let totalItems = $state(data.customs_units?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.customs_units) {
|
||||
allItems = data.customs_units.items || [];
|
||||
currentPage = data.customs_units.page || 1;
|
||||
totalItems = data.customs_units.total || 0;
|
||||
pageSize = data.customs_units.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureCustoms(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
@@ -41,50 +87,92 @@
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureCustoms(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more customs units:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureCustoms(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading customs units:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Unidades de Medida Aduanas MEX</h1>
|
||||
<p class="text-muted-foreground">Catálogo de unidades de medida para aduanas mexicanas</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Unidad
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nueva Unidad</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por clave..." bind:value={searchCode} oninput={handleSearch} />
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
columns={createColumns(handleSuccess)}
|
||||
data={data.customs_units?.items || []}
|
||||
pageCount={data.customs_units?.pages || 0}
|
||||
totalItems={data.customs_units?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Unidades Aduanas MX</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Clave" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /><Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable columns={createColumns(handleSuccess)} data={allItems} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/general/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/general/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/general/create-edit-dialog.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaUnidadesGeneral } from '$lib/config/shortcuts/dashboard/general_catalogs/units_of_measure/general/list';
|
||||
import { getUnitsOfMeasureGeneral } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -30,10 +34,52 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.general_units?.items || []);
|
||||
let currentPage = $state(data.general_units?.page || 1);
|
||||
let pageSize = $state(data.general_units?.page_size || 50);
|
||||
let totalItems = $state(data.general_units?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.general_units) {
|
||||
allItems = data.general_units.items || [];
|
||||
currentPage = data.general_units.page || 1;
|
||||
totalItems = data.general_units.total || 0;
|
||||
pageSize = data.general_units.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureGeneral(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
@@ -41,50 +87,92 @@
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureGeneral(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more general units:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureGeneral(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading general units:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Unidades de Medida</h1>
|
||||
<p class="text-muted-foreground">Catálogo general de unidades de medida</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Unidad
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nueva Unidad</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por clave..." bind:value={searchCode} oninput={handleSearch} />
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
columns={createColumns(handleSuccess)}
|
||||
data={data.general_units?.items || []}
|
||||
pageCount={data.general_units?.pages || 0}
|
||||
totalItems={data.general_units?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Unidades</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Clave" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /><Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable columns={createColumns(handleSuccess)} data={allItems} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/oma/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/oma/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/oma/create-edit-dialog.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaUnidadesOMA } from '$lib/config/shortcuts/dashboard/general_catalogs/units_of_measure/oma/list';
|
||||
import { getUnitsOfMeasureOMA } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -30,10 +34,52 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
let allItems = $state(data.oma_units?.items || []);
|
||||
let currentPage = $state(data.oma_units?.page || 1);
|
||||
let pageSize = $state(data.oma_units?.page_size || 50);
|
||||
let totalItems = $state(data.oma_units?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
|
||||
$effect(() => {
|
||||
if (data.oma_units) {
|
||||
allItems = data.oma_units.items || [];
|
||||
currentPage = data.oma_units.page || 1;
|
||||
totalItems = data.oma_units.total || 0;
|
||||
pageSize = data.oma_units.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureOMA(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('Error applying filters:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
@@ -41,50 +87,92 @@
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureOMA(
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
console.error('Error loading more OMA units:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (searchCode) filters.code = searchCode;
|
||||
if (searchDesc) filters.description = searchDesc;
|
||||
|
||||
const response = await getUnitsOfMeasureOMA(
|
||||
1,
|
||||
pageSize,
|
||||
companyStore.activeCompany.id,
|
||||
filters
|
||||
);
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
console.error('Error reloading OMA units:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
reloadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Unidades de Medida OMA</h1>
|
||||
<p class="text-muted-foreground">Catálogo de unidades de medida OMA</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Unidad
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={reloadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nueva Unidad</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input placeholder="Buscar por clave..." bind:value={searchCode} oninput={handleSearch} />
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
columns={createColumns(handleSuccess)}
|
||||
data={data.oma_units?.items || []}
|
||||
pageCount={data.oma_units?.pages || 0}
|
||||
totalItems={data.oma_units?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Unidades OMA</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Clave" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" /><Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="p-0"><div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable columns={createColumns(handleSuccess)} data={allItems} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
|
||||
// Importar componentes de la librería
|
||||
import DataTable from '$lib/components/dashboard/transportation/vehicles/data-table.svelte';
|
||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/transportation/vehicles/create-edit-dialog.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/transportation/vehicles/columns';
|
||||
|
||||
@@ -17,13 +18,12 @@
|
||||
// --- ESTADO ---
|
||||
let data = $state<Vehicle[]>([]);
|
||||
let totalItems = $state(0);
|
||||
let pageCount = $state(0);
|
||||
let loading = $state(false);
|
||||
let currentPage = $state(1);
|
||||
let pageSize = $state(50);
|
||||
let hasMore = $derived(data.length < totalItems);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
let currentPage = $derived(Number($page.url.searchParams.get('page')) || 1);
|
||||
let pageSize = 10;
|
||||
|
||||
// Filtros
|
||||
let searchKey = $state($page.url.searchParams.get('vehicle_key') || '');
|
||||
let searchPlate = $state($page.url.searchParams.get('plate_number') || '');
|
||||
@@ -35,7 +35,7 @@
|
||||
loading = true;
|
||||
try {
|
||||
const response = await vehiclesApi.list(companyStore.activeCompany.id, {
|
||||
page: currentPage,
|
||||
page: 1,
|
||||
page_size: pageSize,
|
||||
vehicle_key: searchKey,
|
||||
plate_number: searchPlate
|
||||
@@ -43,8 +43,8 @@
|
||||
|
||||
if (response.data) {
|
||||
data = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
pageCount = Math.ceil(response.data.total / response.data.page_size);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading vehicles:', error);
|
||||
@@ -53,12 +53,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
try {
|
||||
const response = await vehiclesApi.list(companyStore.activeCompany.id, {
|
||||
page: currentPage + 1,
|
||||
page_size: pageSize,
|
||||
vehicle_key: searchKey,
|
||||
plate_number: searchPlate
|
||||
});
|
||||
|
||||
if (response.data?.items) {
|
||||
data = [...data, ...response.data.items];
|
||||
currentPage += 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading more vehicles:', error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', '1');
|
||||
|
||||
if (searchKey) url.searchParams.set('vehicle_key', searchKey);
|
||||
else url.searchParams.delete('vehicle_key');
|
||||
@@ -79,41 +101,23 @@
|
||||
const columns = createColumns(loadData);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col space-y-6 p-8">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Vehículos (Transporte)</h2>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Vehículos (Transporte)</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestión del catálogo de camiones y vehículos de transporte
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Vehículo
|
||||
</Button>
|
||||
<div class="flex items-center gap-3"><Button variant="outline" size="sm" class="h-9" onclick={loadData}><RefreshCw class="mr-2 h-4 w-4" />Actualizar</Button><Button class="h-9" onclick={() => (createDialogOpen = true)}><Plus class="mr-2 h-4 w-4" />Nuevo Vehículo</Button></div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<Input
|
||||
placeholder="Buscar por clave..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchKey}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Buscar por placas..."
|
||||
class="h-8 w-[250px] bg-card"
|
||||
bind:value={searchPlate}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Vehículos</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Clave" class="h-9 w-40 bg-card lg:w-52" bind:value={searchKey} oninput={handleSearch} /><Input placeholder="Placas" class="h-9 w-40 bg-card lg:w-52" bind:value={searchPlate} oninput={handleSearch} /></div></div></Card.Header>
|
||||
<Card.Content class="p-0">{#if loading && data.length === 0}<div class="flex h-64 items-center justify-center text-muted-foreground">Cargando vehículos...</div>{:else}<div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable {data} {columns} {loading} {hasMore} {loadMore} /></div>{/if}</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
{#if loading && data.length === 0}
|
||||
<div class="flex h-64 items-center justify-center text-muted-foreground">
|
||||
Cargando vehículos...
|
||||
</div>
|
||||
{:else}
|
||||
<DataTable {data} {columns} {pageCount} {totalItems} />
|
||||
{/if}
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {data.length} de {totalItems} registros</div>
|
||||
|
||||
<CreateEditDialog bind:open={createDialogOpen} onSuccess={loadData} />
|
||||
</div>
|
||||
|
||||
@@ -233,19 +233,30 @@
|
||||
]);
|
||||
</script>
|
||||
|
||||
<div class="flex h-[calc(100vh-4rem)] flex-col gap-4 p-4 pb-15">
|
||||
<!-- Título -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<h1 class="text-2xl font-bold">CATALOGO DE CLASES DE ACTIVO FIJO</h1>
|
||||
<p class="text-sm text-muted-foreground">Gestiona y consulta las clases de activo fijo</p>
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Clases de Activo Fijo</h1>
|
||||
<p class="text-muted-foreground">Gestiona y consulta las clases de activo fijo</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={handleRefresh}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button class="h-9" onclick={handleNew}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Clase
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenedor principal con grid y detalles -->
|
||||
<div class="flex flex-1 gap-4 overflow-hidden">
|
||||
<div class="flex min-h-0 flex-1 gap-4 overflow-hidden">
|
||||
<!-- Panel izquierdo: Grid/Tabla de clases -->
|
||||
<div class="flex flex-1 flex-col gap-4 overflow-hidden">
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden">
|
||||
<!-- Sección de Filtros -->
|
||||
<div class="rounded-lg border bg-card">
|
||||
<div class="rounded-md border bg-background">
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold">Filtros</h2>
|
||||
@@ -256,37 +267,37 @@
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">Clase</Label>
|
||||
<Input bind:value={searchTerm} placeholder="Ej: AF001" class="h-9" />
|
||||
<Input bind:value={searchTerm} placeholder="Ej: AF001" class="h-9 bg-card" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">Descripción</Label>
|
||||
<Input
|
||||
bind:value={searchDescription}
|
||||
placeholder="Buscar descripción..."
|
||||
class="h-9"
|
||||
class="h-9 bg-card"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">Tipo</Label>
|
||||
<Input bind:value={searchType} placeholder="MP, SC, DESP..." class="h-9" />
|
||||
<Input bind:value={searchType} placeholder="MP, SC, DESP..." class="h-9 bg-card" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">Fracción</Label>
|
||||
<Input bind:value={searchFraction} placeholder="Fracción arancelaria" class="h-9" />
|
||||
<Input bind:value={searchFraction} placeholder="Fracción arancelaria" class="h-9 bg-card" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de clases -->
|
||||
<div class="flex flex-1 flex-col overflow-hidden rounded-lg border">
|
||||
<div class="flex items-center justify-between border-b bg-white p-3 dark:bg-muted/50">
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border bg-background">
|
||||
<div class="flex items-center justify-between border-b bg-background/95 p-3">
|
||||
<h2 class="text-sm font-semibold">Listado de Clases</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
Mostrando de {filteredClasses.length} registros
|
||||
</span>
|
||||
<Button variant="outline" size="sm" onclick={handleRefresh}>
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={handleRefresh}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
@@ -309,9 +320,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Panel derecho: Detalles y edición -->
|
||||
<div
|
||||
class="flex w-96 flex-none flex-col overflow-hidden rounded-xl border bg-muted/30 shadow-sm"
|
||||
>
|
||||
<div class="flex w-96 flex-none flex-col overflow-hidden rounded-xl border bg-muted/30 shadow-sm">
|
||||
<div class="border-b p-4">
|
||||
<p class="text-[10px] tracking-widest text-muted-foreground uppercase opacity-80">
|
||||
Código de Clase
|
||||
@@ -382,6 +391,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-20"></div>
|
||||
|
||||
<!-- Footer fijo con botones de acción -->
|
||||
<div
|
||||
class="fixed right-0 bottom-0 left-0 z-[5] border-t bg-background/95 shadow-lg backdrop-blur supports-[backdrop-filter]:bg-background/80"
|
||||
|
||||
@@ -190,21 +190,32 @@
|
||||
]);
|
||||
</script>
|
||||
|
||||
<div class="flex h-[calc(100vh-4rem)] flex-col gap-4 p-4 pb-15">
|
||||
<!-- Título -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<h1 class="text-2xl font-bold">CATÁLOGO DE PARTES</h1>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Gestiona y consulta las partes de inventario y activo fijo
|
||||
</p>
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Catálogo de Partes</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona y consulta las partes de inventario y activo fijo
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={handleRefresh}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
<Button class="h-9" href="/dashboard/goods/parts/edit">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Parte
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenedor principal con grid y detalles -->
|
||||
<div class="flex flex-1 gap-4 overflow-hidden">
|
||||
<div class="flex min-h-0 flex-1 gap-4 overflow-hidden">
|
||||
<!-- Panel izquierdo: Grid/Tabla de partes -->
|
||||
<div class="flex flex-1 flex-col gap-4 overflow-hidden">
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden">
|
||||
<!-- Sección de Filtros -->
|
||||
<div class="rounded-lg border bg-card">
|
||||
<div class="rounded-md border bg-background">
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold">Filtros</h2>
|
||||
@@ -215,23 +226,23 @@
|
||||
<div class="grid grid-cols-5 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">Número de Parte</Label>
|
||||
<Input bind:value={searchPartNumber} placeholder="Ej: PART-001" class="h-9" />
|
||||
<Input bind:value={searchPartNumber} placeholder="Ej: PART-001" class="h-9 bg-card" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">Descripción</Label>
|
||||
<Input
|
||||
bind:value={searchDescription}
|
||||
placeholder="Buscar descripción..."
|
||||
class="h-9"
|
||||
class="h-9 bg-card"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">Cliente</Label>
|
||||
<Input bind:value={searchClient} placeholder="Nombre o ID..." class="h-9" />
|
||||
<Input bind:value={searchClient} placeholder="Nombre o ID..." class="h-9 bg-card" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">Clase</Label>
|
||||
<Input bind:value={searchClass} placeholder="Clase..." class="h-9" />
|
||||
<Input bind:value={searchClass} placeholder="Clase..." class="h-9 bg-card" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">Sistema</Label>
|
||||
@@ -267,14 +278,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Tabla de partes -->
|
||||
<div class="flex flex-1 flex-col overflow-hidden rounded-lg border">
|
||||
<div class="flex items-center justify-between border-b bg-white p-3 dark:bg-muted/50">
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border bg-background">
|
||||
<div class="flex items-center justify-between border-b bg-background/95 p-3">
|
||||
<h2 class="text-sm font-semibold">Listado de Partes</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
Mostrando {filteredParts.length} registros
|
||||
</span>
|
||||
<Button variant="outline" size="sm" onclick={handleRefresh}>
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={handleRefresh}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
@@ -298,9 +309,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Panel derecho: Detalles y edición -->
|
||||
<div
|
||||
class="flex w-96 flex-none flex-col overflow-hidden rounded-xl border bg-muted/30 shadow-sm"
|
||||
>
|
||||
<div class="flex w-96 flex-none flex-col overflow-hidden rounded-xl border bg-muted/30 shadow-sm">
|
||||
<div class="border-b p-4">
|
||||
<p class="text-[10px] tracking-widest text-muted-foreground uppercase opacity-80">
|
||||
Número de Parte
|
||||
@@ -414,6 +423,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-20"></div>
|
||||
|
||||
<!-- Footer fijo con botones de acción -->
|
||||
<div
|
||||
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
|
||||
|
||||
@@ -863,17 +863,17 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Facturas</h1>
|
||||
<p class="text-muted-foreground">Gestiona las facturas del sistema</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<select
|
||||
id="filter-operation-type"
|
||||
bind:value={filters.operation_type}
|
||||
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"
|
||||
class="flex h-9 w-[180px] rounded-md border border-input bg-card px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
title="Tipo de Operación"
|
||||
>
|
||||
{#each operationTypeOptions as option}
|
||||
@@ -884,7 +884,7 @@
|
||||
<select
|
||||
id="filter-invoice-type"
|
||||
bind:value={filters.invoice_type}
|
||||
class="flex h-9 w-[220px] 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"
|
||||
class="flex h-9 w-[220px] rounded-md border border-input bg-card px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
title="Tipo de Factura"
|
||||
>
|
||||
{#each invoiceTypeOptions() as option}
|
||||
@@ -914,32 +914,29 @@
|
||||
</Card.Root>
|
||||
{/if}
|
||||
|
||||
<Card.Root>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Listado de Facturas</Card.Title>
|
||||
<Card.Description>
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</Card.Description>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
bind:value={filters.invoice_number}
|
||||
placeholder="No. Factura"
|
||||
class="w-32 lg:w-48 h-9"
|
||||
class="w-32 lg:w-48 h-9 bg-card"
|
||||
/>
|
||||
<Input
|
||||
bind:value={filters.year_from}
|
||||
placeholder="Año inicio"
|
||||
class="w-24 h-9"
|
||||
class="w-24 h-9 bg-card"
|
||||
maxlength={4}
|
||||
/>
|
||||
<span class="text-sm text-muted-foreground">-</span>
|
||||
<Input
|
||||
bind:value={filters.year_to}
|
||||
placeholder="Año fin"
|
||||
class="w-24 h-9"
|
||||
class="w-24 h-9 bg-card"
|
||||
maxlength={4}
|
||||
/>
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
@@ -949,21 +946,31 @@
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<DataTable
|
||||
data={allItems}
|
||||
columns={createColumns()}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
selectedIds={selectedInvoiceIds}
|
||||
onRowClick={handleRowClick}
|
||||
{sorting}
|
||||
onSortingChange={(newSorting) => (sorting = newSorting)}
|
||||
/>
|
||||
<Card.Content class="p-0">
|
||||
<div class="rounded-md border bg-background">
|
||||
<DataTable
|
||||
data={allItems}
|
||||
columns={createColumns()}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
selectedIds={selectedInvoiceIds}
|
||||
onRowClick={handleRowClick}
|
||||
{sorting}
|
||||
onSortingChange={(newSorting) => (sorting = newSorting)}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
<span class="ml-2">•</span>
|
||||
<span class="ml-2">Filtros activos: {Object.values(filters).filter((value) => value !== '').length}</span>
|
||||
</div>
|
||||
|
||||
<div class="h-20"></div>
|
||||
|
||||
<PdfProgressDialog
|
||||
bind:open={showProgressDialog}
|
||||
taskId={currentTaskId}
|
||||
|
||||
@@ -439,11 +439,11 @@ let filters = $state({
|
||||
const columns = createColumns(handleSuccess);
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Pedimentos</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Pedimentos</h1>
|
||||
<p class="text-muted-foreground">Gestiona los pedimentos del sistema</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@@ -451,7 +451,7 @@ let filters = $state({
|
||||
id="filter-status"
|
||||
bind:value={filters.status}
|
||||
onchange={applyFilters}
|
||||
class="flex h-9 w-[220px] 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"
|
||||
class="flex h-9 w-[220px] rounded-md border border-input bg-card 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"
|
||||
>
|
||||
{#each statusOptions as option}
|
||||
@@ -478,14 +478,11 @@ let filters = $state({
|
||||
{/if}
|
||||
|
||||
<!-- Data Table -->
|
||||
<Card.Root>
|
||||
<Card.Root class="border bg-background flex flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Listado de Pedimentos</Card.Title>
|
||||
<Card.Description>
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</Card.Description>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
@@ -493,7 +490,7 @@ let filters = $state({
|
||||
bind:value={filters.pedimento}
|
||||
oninput={applyFilters}
|
||||
placeholder="Buscar pedimento"
|
||||
class="w-40 lg:w-56 h-9"
|
||||
class="w-32 lg:w-48 h-9 bg-card"
|
||||
/>
|
||||
<Input
|
||||
id="filter-year"
|
||||
@@ -501,7 +498,7 @@ let filters = $state({
|
||||
oninput={applyFilters}
|
||||
placeholder="Año"
|
||||
maxlength={2}
|
||||
class="w-20 h-9"
|
||||
class="w-24 h-9 bg-card"
|
||||
/>
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
@@ -510,22 +507,32 @@ let filters = $state({
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<Card.Content class="p-0">
|
||||
<!-- TanStack DataTable con Infinite Scroll -->
|
||||
<DataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
{selectedId}
|
||||
onRowClick={handleRowClick}
|
||||
{sorting}
|
||||
onSortingChange={(newSorting) => (sorting = newSorting)}
|
||||
/>
|
||||
<div class="rounded-md border bg-background">
|
||||
<DataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
{selectedId}
|
||||
onRowClick={handleRowClick}
|
||||
{sorting}
|
||||
onSortingChange={(newSorting) => (sorting = newSorting)}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
<span class="ml-2">•</span>
|
||||
<span class="ml-2">Filtros activos: {Object.values(filters).filter((value) => value !== '').length}</span>
|
||||
</div>
|
||||
|
||||
<div class="h-20"></div>
|
||||
|
||||
<!-- Footer fijo con botones de acción -->
|
||||
<div
|
||||
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -117,7 +118,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Pedimento - Regímenes
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -132,33 +133,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Pedimento - Regímenes</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,7 +113,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Contenedores
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -127,27 +128,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Contenedores</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
@@ -149,7 +150,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Países
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -164,33 +165,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Países</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,7 +113,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Tipos de Moneda
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -127,27 +128,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Tipos de Moneda</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,7 +113,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Secciones Aduanales
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -127,27 +128,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Secciones Aduanales</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,7 +113,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Recintos Fiscalizados
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -127,27 +128,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Recintos Fiscalizados</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
@@ -116,7 +117,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Incoterms
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -131,42 +132,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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="Filtro por código..."
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
class="h-9"
|
||||
/>
|
||||
</div>
|
||||
<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 del término..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
class="h-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable
|
||||
columns={createColumns(handleSuccess)}
|
||||
data={allItems}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Incoterms</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Clave" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" />
|
||||
<Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable columns={createColumns(handleSuccess)} data={allItems} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,7 +113,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Tipos de Factura
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -127,27 +128,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Tipos de Factura</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,7 +113,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Tipos de Material
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -127,27 +128,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Tipos de Material</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,7 +113,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Métodos de Pago
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -127,27 +128,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Métodos de Pago</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,7 +113,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Claves de Pedimento
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -127,27 +128,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Claves de Pedimento</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,7 +113,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Regímenes
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -127,27 +128,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Regímenes</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -136,7 +137,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Sectores
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -151,27 +152,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Sectores</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * 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';
|
||||
@@ -146,7 +147,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Estados
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -161,33 +162,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Estados</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,7 +113,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Modos de Transporte
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -127,27 +128,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Modos de Transporte</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,7 +113,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Tipos de Transporte
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -127,27 +128,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Tipos de Transporte</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,7 +113,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Métodos de Valoración
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
@@ -127,27 +128,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="flex-none 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="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 min-h-0 rounded-md border bg-background overflow-hidden flex flex-col">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Métodos de Valoración</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user