refactor: simplify column creation in dashboard components and improve error handling
- Removed derived error state in manifest page. - Updated column creation logic in various general catalog components to streamline permissions handling. - Enhanced type safety in selected item logic across components. - Improved search functionality and state management in identifiers, concepts, and other catalog pages.
This commit is contained in:
@@ -47,10 +47,8 @@
|
|||||||
let showDetails = $state(false);
|
let showDetails = $state(false);
|
||||||
|
|
||||||
|
|
||||||
const isError = $derived(!canView || status >= 400 || error);
|
|
||||||
|
|
||||||
// Pasamos canEdit y canDelete a las columnas por si tu DataTable tiene acciones por fila
|
// Pasamos canEdit y canDelete a las columnas por si tu DataTable tiene acciones por fila
|
||||||
const columns = createColumns({ canEdit, canDelete });
|
const columns = createColumns();
|
||||||
|
|
||||||
// --- Lifecycle ---
|
// --- Lifecycle ---
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|||||||
@@ -1,238 +1,285 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/concepts/columns';
|
import { createColumns } from '$lib/components/dashboard/general_catalogs/concepts/columns';
|
||||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/concepts/create-edit-dialog.svelte';
|
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/concepts/create-edit-dialog.svelte';
|
||||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
||||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||||
import { obtenerAtajosListaConceptos } from '$lib/config/shortcuts/dashboard/general_catalogs/concepts/list';
|
import { obtenerAtajosListaConceptos } from '$lib/config/shortcuts/dashboard/general_catalogs/concepts/list';
|
||||||
import { getConcepts, deleteConcept, type Concept } from '$lib/api/dashboard/a76/general_catalogs/concepts';
|
import {
|
||||||
import { companyStore } from '$lib/stores/company.svelte';
|
getConcepts,
|
||||||
import { currentUser, userHasPermission } from '$lib/auth';
|
deleteConcept,
|
||||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
type Concept
|
||||||
|
} from '$lib/api/dashboard/a76/general_catalogs/concepts';
|
||||||
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
|
import { currentUser, userHasPermission } from '$lib/auth';
|
||||||
|
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let dialogOpen = $state(false);
|
let dialogOpen = $state(false);
|
||||||
let editingItem = $state<Concept | null>(null);
|
let editingItem = $state<Concept | null>(null);
|
||||||
let error = $state<string | null>(data.error || null);
|
let error = $state<string | null>(data.error || null);
|
||||||
let status = $state<number>(data.status || 200);
|
let status = $state<number>(data.status || 200);
|
||||||
|
|
||||||
// Permisos
|
// Permisos
|
||||||
const canView = $derived(userHasPermission($currentUser, 'cat_concepts.view'));
|
const canView = $derived(userHasPermission($currentUser, 'cat_concepts.view'));
|
||||||
const canCreate = $derived(userHasPermission($currentUser, 'cat_concepts.create'));
|
const canCreate = $derived(userHasPermission($currentUser, 'cat_concepts.create'));
|
||||||
const canEdit = $derived(userHasPermission($currentUser, 'cat_concepts.edit'));
|
const canEdit = $derived(userHasPermission($currentUser, 'cat_concepts.edit'));
|
||||||
const canDelete = $derived(userHasPermission($currentUser, 'cat_concepts.delete'));
|
const canDelete = $derived(userHasPermission($currentUser, 'cat_concepts.delete'));
|
||||||
|
|
||||||
const isError = $derived(!canView || status >= 400 || error);
|
const isError = $derived(!canView || status >= 400 || error);
|
||||||
|
|
||||||
// Atajos
|
// Atajos
|
||||||
useShortcuts(
|
useShortcuts(
|
||||||
'Lista Conceptos',
|
'Lista Conceptos',
|
||||||
obtenerAtajosListaConceptos({
|
obtenerAtajosListaConceptos({
|
||||||
manejarNuevo: () => {
|
manejarNuevo: () => {
|
||||||
if (!canCreate) return;
|
if (!canCreate) return;
|
||||||
editingItem = null;
|
editingItem = null;
|
||||||
dialogOpen = true;
|
dialogOpen = true;
|
||||||
},
|
},
|
||||||
manejarActualizar: () => reloadData()
|
manejarActualizar: () => reloadData()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filtros
|
// Filtros
|
||||||
let searchKey = $state($page.url.searchParams.get('code') || '');
|
let searchKey = $state($page.url.searchParams.get('code') || '');
|
||||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
let allItems = $state<Concept[]>(data.items?.items || data.items || []);
|
let allItems = $state<Concept[]>(data.concepts?.items || data.concepts || []);
|
||||||
let currentPage = $state(data.items?.page || 1);
|
let currentPage = $state(data.concepts?.page || 1);
|
||||||
let pageSize = $state(data.items?.page_size || 50);
|
let pageSize = $state(data.concepts?.page_size || 50);
|
||||||
let totalItems = $state(data.items?.total || 0);
|
let totalItems = $state(data.concepts?.total || 0);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let hasMore = $derived(allItems.length < totalItems);
|
let hasMore = $derived(allItems.length < totalItems);
|
||||||
let selectedIds = $state<(string | number)[]>([]);
|
let selectedIds = $state<(string | number)[]>([]);
|
||||||
const selectedItem = $derived(
|
const selectedItem = $derived(
|
||||||
selectedIds.length === 1
|
selectedIds.length === 1
|
||||||
? allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null
|
? (allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (data.items) {
|
if (data.concepts) {
|
||||||
allItems = data.items.items || data.items || [];
|
allItems = data.concepts.items || data.concepts || [];
|
||||||
currentPage = data.items.page || 1;
|
currentPage = data.concepts.page || 1;
|
||||||
totalItems = data.items.total || 0;
|
totalItems = data.concepts.total || 0;
|
||||||
pageSize = data.items.page_size || pageSize;
|
pageSize = data.concepts.page_size || pageSize;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleSearch() {
|
async function handleSearch() {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(async () => {
|
timeout = setTimeout(async () => {
|
||||||
if (!companyStore.activeCompany) return;
|
if (!companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await getConcepts(1, pageSize, companyStore.activeCompany.id, {
|
const response = await getConcepts(1, pageSize, companyStore.activeCompany.id, {
|
||||||
code: searchKey || undefined,
|
code: searchKey || undefined,
|
||||||
description: searchDesc || undefined
|
description: searchDesc || undefined
|
||||||
});
|
});
|
||||||
const payload = (response as any).items ? response : (response as any).data;
|
const payload = (response as any).items ? response : (response as any).data;
|
||||||
if (payload?.items) {
|
if (payload?.items) {
|
||||||
allItems = payload.items;
|
allItems = payload.items;
|
||||||
currentPage = payload.page || 1;
|
currentPage = payload.page || 1;
|
||||||
totalItems = payload.total;
|
totalItems = payload.total;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Error aplicando filtros';
|
error = 'Error aplicando filtros';
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL($page.url);
|
const url = new URL($page.url);
|
||||||
if (searchKey) url.searchParams.set('code', searchKey);
|
if (searchKey) url.searchParams.set('code', searchKey);
|
||||||
else url.searchParams.delete('code');
|
else url.searchParams.delete('code');
|
||||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||||
else url.searchParams.delete('description');
|
else url.searchParams.delete('description');
|
||||||
history.replaceState(history.state, '', url);
|
history.replaceState(history.state, '', url);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMore() {
|
async function loadMore() {
|
||||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await getConcepts(currentPage + 1, pageSize, companyStore.activeCompany.id, {
|
const response = await getConcepts(currentPage + 1, pageSize, companyStore.activeCompany.id, {
|
||||||
code: searchKey || undefined,
|
code: searchKey || undefined,
|
||||||
description: searchDesc || undefined
|
description: searchDesc || undefined
|
||||||
});
|
});
|
||||||
const payload = (response as any).items ? response : (response as any).data;
|
const payload = (response as any).items ? response : (response as any).data;
|
||||||
if (payload?.items) {
|
if (payload?.items) {
|
||||||
allItems = [...allItems, ...payload.items];
|
allItems = [...allItems, ...payload.items];
|
||||||
currentPage = payload.page || (currentPage + 1);
|
currentPage = payload.page || currentPage + 1;
|
||||||
totalItems = payload.total;
|
totalItems = payload.total;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Error cargando mas datos';
|
error = 'Error cargando mas datos';
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadData() {
|
async function reloadData() {
|
||||||
if (!companyStore.activeCompany) return;
|
if (!companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await getConcepts(1, pageSize, companyStore.activeCompany.id, {
|
const response = await getConcepts(1, pageSize, companyStore.activeCompany.id, {
|
||||||
code: searchKey || undefined,
|
code: searchKey || undefined,
|
||||||
description: searchDesc || undefined
|
description: searchDesc || undefined
|
||||||
});
|
});
|
||||||
const payload = (response as any).items ? response : (response as any).data;
|
const payload = (response as any).items ? response : (response as any).data;
|
||||||
if (payload?.items) {
|
if (payload?.items) {
|
||||||
allItems = payload.items;
|
allItems = payload.items;
|
||||||
currentPage = payload.page || 1;
|
currentPage = payload.page || 1;
|
||||||
totalItems = payload.total;
|
totalItems = payload.total;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Error al recargar datos';
|
error = 'Error al recargar datos';
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSuccess() {
|
function handleSuccess() {
|
||||||
dialogOpen = false;
|
dialogOpen = false;
|
||||||
editingItem = null;
|
editingItem = null;
|
||||||
selectedIds = [];
|
selectedIds = [];
|
||||||
reloadData();
|
reloadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]">
|
<div
|
||||||
<div class="flex flex-none items-center justify-between">
|
class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]"
|
||||||
<div>
|
>
|
||||||
<h1 class="text-2xl font-bold tracking-tight">Conceptos</h1>
|
<div class="flex flex-none items-center justify-between">
|
||||||
<p class="text-muted-foreground">Catálogo de Conceptos</p>
|
<div>
|
||||||
</div>
|
<h1 class="text-2xl font-bold tracking-tight">Conceptos</h1>
|
||||||
<div class="flex items-center gap-3">
|
<p class="text-muted-foreground">Catálogo de Conceptos</p>
|
||||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
</div>
|
||||||
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
<div class="flex items-center gap-3">
|
||||||
</Button>
|
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||||
{#if !isError && canCreate}
|
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
||||||
<Button class="h-9" onclick={() => { editingItem = null; dialogOpen = true; }}>
|
</Button>
|
||||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
{#if !isError && canCreate}
|
||||||
</Button>
|
<Button
|
||||||
{/if}
|
class="h-9"
|
||||||
</div>
|
onclick={() => {
|
||||||
</div>
|
editingItem = null;
|
||||||
|
dialogOpen = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if isError}
|
{#if isError}
|
||||||
<ErrorState
|
<ErrorState
|
||||||
status={!canView ? 403 : status}
|
status={!canView ? 403 : status}
|
||||||
error={!canView ? 'Permission denied: cat_concepts.view' : error || ''}
|
error={!canView ? 'Permission denied: cat_concepts.view' : error || ''}
|
||||||
onRetry={reloadData}
|
onRetry={reloadData}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<Card.Title>Listado</Card.Title>
|
<Card.Title>Listado</Card.Title>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<Input placeholder="Código" bind:value={searchKey} oninput={handleSearch} class="h-9 w-40 bg-card lg:w-52" />
|
<Input
|
||||||
<Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
placeholder="Código"
|
||||||
</div>
|
bind:value={searchKey}
|
||||||
</div>
|
oninput={handleSearch}
|
||||||
</Card.Header>
|
class="h-9 w-40 bg-card lg:w-52"
|
||||||
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
/>
|
||||||
<div class="h-full overflow-hidden rounded-md border bg-background">
|
<Input
|
||||||
<InfiniteDataTable
|
placeholder="Descripción"
|
||||||
data={allItems} {columns} {loading} {hasMore} {loadMore}
|
bind:value={searchDesc}
|
||||||
{selectedIds} onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
oninput={handleSearch}
|
||||||
onRowClick={(row) => selectedIds = selectedIds.includes(row.id) ? [] : [row.id]}
|
class="h-9 w-44 bg-card lg:w-64"
|
||||||
onRowDoubleClick={(row) => { if(canEdit) { editingItem = row; dialogOpen = true; } }}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Header>
|
||||||
</Card.Root>
|
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
||||||
|
<div class="h-full overflow-hidden rounded-md border bg-background">
|
||||||
|
<InfiniteDataTable
|
||||||
|
data={allItems}
|
||||||
|
{columns}
|
||||||
|
{loading}
|
||||||
|
{hasMore}
|
||||||
|
{loadMore}
|
||||||
|
{selectedIds}
|
||||||
|
onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
||||||
|
onRowClick={(row) => (selectedIds = selectedIds.includes(row.id) ? [] : [row.id])}
|
||||||
|
onRowDoubleClick={(row) => {
|
||||||
|
if (canEdit) {
|
||||||
|
editingItem = row;
|
||||||
|
dialogOpen = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
<div class="flex-none text-sm text-muted-foreground">
|
<div class="flex-none text-sm text-muted-foreground">
|
||||||
Mostrando {allItems.length} de {totalItems} registros
|
Mostrando {allItems.length} de {totalItems} registros
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed right-0 bottom-0 left-0 z-50 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"
|
||||||
|
>
|
||||||
|
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
{#if canEdit}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => {
|
||||||
|
editingItem = selectedItem;
|
||||||
|
dialogOpen = true;
|
||||||
|
}}
|
||||||
|
disabled={!selectedItem}
|
||||||
|
>
|
||||||
|
<Pencil size={16} class="mr-2" /> Editar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if canDelete}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={async () => {
|
||||||
|
if (!selectedItem || !companyStore.activeCompany) return;
|
||||||
|
if (confirm('¿Eliminar este registro?')) {
|
||||||
|
await deleteConcept(selectedItem.id, companyStore.activeCompany.id);
|
||||||
|
selectedIds = [];
|
||||||
|
reloadData();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!selectedItem}
|
||||||
|
class="text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} class="mr-2" /> Eliminar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="fixed right-0 bottom-0 left-0 z-50 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">
|
<CreateEditDialog bind:open={dialogOpen} item={editingItem} onSuccess={handleSuccess} />
|
||||||
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
{#if canEdit}
|
|
||||||
<Button variant="outline" size="sm" onclick={() => { editingItem = selectedItem; dialogOpen = true; }} disabled={!selectedItem}>
|
|
||||||
<Pencil size={16} class="mr-2" /> Editar
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{#if canDelete}
|
|
||||||
<Button variant="outline" size="sm" onclick={async () => {
|
|
||||||
if(!selectedItem || !companyStore.activeCompany) return;
|
|
||||||
if(confirm('¿Eliminar este registro?')) {
|
|
||||||
await deleteConcept(selectedItem.id, companyStore.activeCompany.id);
|
|
||||||
selectedIds = [];
|
|
||||||
reloadData();
|
|
||||||
}
|
|
||||||
}} disabled={!selectedItem} class="text-destructive hover:bg-destructive/10">
|
|
||||||
<Trash2 size={16} class="mr-2" /> Eliminar
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<CreateEditDialog bind:open={dialogOpen} item={editingItem} onSuccess={handleSuccess} />
|
|
||||||
</div>
|
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
let selectedIds = $state<(string | number)[]>([]);
|
let selectedIds = $state<(string | number)[]>([]);
|
||||||
const selectedItem = $derived(
|
const selectedItem = $derived(
|
||||||
selectedIds.length === 1 ? allItems.find((item) => item.id === selectedIds[0]) ?? null : null
|
selectedIds.length === 1 ? allItems.find((item: ExchangeRate) => item.id === selectedIds[0]) ?? null : null
|
||||||
);
|
);
|
||||||
|
|
||||||
// Permisos
|
// Permisos
|
||||||
|
|||||||
@@ -1,237 +1,283 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/identifiers/columns';
|
import { createColumns } from '$lib/components/dashboard/general_catalogs/identifiers/columns';
|
||||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/identifiers/create-edit-dialog.svelte';
|
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/identifiers/create-edit-dialog.svelte';
|
||||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
||||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||||
import { obtenerAtajosListaIdentificadores } from '$lib/config/shortcuts/dashboard/general_catalogs/identifiers/list';
|
import { obtenerAtajosListaIdentificadores } from '$lib/config/shortcuts/dashboard/general_catalogs/identifiers/list';
|
||||||
import { identifiersApi, type Identifier } from '$lib/api/dashboard/a76/general_catalogs/identifiers';
|
import {
|
||||||
import { companyStore } from '$lib/stores/company.svelte';
|
identifiersApi,
|
||||||
import { currentUser, userHasPermission } from '$lib/auth';
|
type Identifier
|
||||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
} from '$lib/api/dashboard/a76/general_catalogs/identifiers';
|
||||||
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
|
import { currentUser, userHasPermission } from '$lib/auth';
|
||||||
|
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let createDialogOpen = $state(false);
|
let createDialogOpen = $state(false);
|
||||||
let editingItem = $state<Identifier | null>(null);
|
let editingItem = $state<Identifier | null>(null);
|
||||||
let error = $state<string | null>(data.error || null);
|
let error = $state<string | null>(data.error || null);
|
||||||
let status = $state<number>(data.status || 200);
|
let status = $state<number>(data.status || 200);
|
||||||
|
|
||||||
// Permisos
|
// Permisos
|
||||||
const canView = $derived(userHasPermission($currentUser, 'cat_identifiers.view'));
|
const canView = $derived(userHasPermission($currentUser, 'cat_identifiers.view'));
|
||||||
const canCreate = $derived(userHasPermission($currentUser, 'cat_identifiers.create'));
|
const canCreate = $derived(userHasPermission($currentUser, 'cat_identifiers.create'));
|
||||||
const canEdit = $derived(userHasPermission($currentUser, 'cat_identifiers.edit'));
|
const canEdit = $derived(userHasPermission($currentUser, 'cat_identifiers.edit'));
|
||||||
const canDelete = $derived(userHasPermission($currentUser, 'cat_identifiers.delete'));
|
const canDelete = $derived(userHasPermission($currentUser, 'cat_identifiers.delete'));
|
||||||
|
|
||||||
const isError = $derived(!canView || status >= 400 || error);
|
const isError = $derived(!canView || status >= 400 || error);
|
||||||
|
|
||||||
// Atajos
|
// Atajos
|
||||||
useShortcuts(
|
useShortcuts(
|
||||||
'Lista Identificadores',
|
'Lista Identificadores',
|
||||||
obtenerAtajosListaIdentificadores({
|
obtenerAtajosListaIdentificadores({
|
||||||
manejarNuevo: () => {
|
manejarNuevo: () => {
|
||||||
if (!canCreate) return;
|
if (!canCreate) return;
|
||||||
editingItem = null;
|
editingItem = null;
|
||||||
createDialogOpen = true;
|
createDialogOpen = true;
|
||||||
},
|
},
|
||||||
manejarActualizar: () => reloadData()
|
manejarActualizar: () => reloadData()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filtros
|
// Filtros
|
||||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
let allItems = $state<Identifier[]>(data.items?.items || data.items || []);
|
let allItems = $state<Identifier[]>(data.identifiers?.items || data.identifiers || []);
|
||||||
let currentPage = $state(data.items?.page || 1);
|
let currentPage = $state(data.identifiers?.page || 1);
|
||||||
let pageSize = $state(data.items?.page_size || 50);
|
let pageSize = $state(data.identifiers?.page_size || 50);
|
||||||
let totalItems = $state(data.items?.total || 0);
|
let totalItems = $state(data.identifiers?.total || 0);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let hasMore = $derived(allItems.length < totalItems);
|
let hasMore = $derived(allItems.length < totalItems);
|
||||||
let selectedIds = $state<(string | number)[]>([]);
|
let selectedIds = $state<(string | number)[]>([]);
|
||||||
const selectedItem = $derived(
|
const selectedItem = $derived(
|
||||||
selectedIds.length === 1
|
selectedIds.length === 1
|
||||||
? allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null
|
? (allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (data.items) {
|
if (data.identifiers) {
|
||||||
allItems = data.items.items || data.items || [];
|
allItems = data.identifiers.items || data.identifiers || [];
|
||||||
currentPage = data.items.page || 1;
|
currentPage = data.identifiers.page || 1;
|
||||||
totalItems = data.items.total || 0;
|
totalItems = data.identifiers.total || 0;
|
||||||
pageSize = data.items.page_size || pageSize;
|
pageSize = data.identifiers.page_size || pageSize;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleSearch() {
|
async function handleSearch() {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(async () => {
|
timeout = setTimeout(async () => {
|
||||||
if (!companyStore.activeCompany) return;
|
if (!companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await identifiersApi.list(companyStore.activeCompany.id, {
|
const response = await identifiersApi.list(companyStore.activeCompany.id, {
|
||||||
code: searchCode || undefined,
|
code: searchCode || undefined,
|
||||||
description: searchDesc || undefined,
|
description: searchDesc || undefined,
|
||||||
page: '1',
|
page: '1',
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
allItems = response.data.items;
|
allItems = response.data.items;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Error aplicando filtros';
|
error = 'Error aplicando filtros';
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL($page.url);
|
const url = new URL($page.url);
|
||||||
if (searchCode) url.searchParams.set('code', searchCode);
|
if (searchCode) url.searchParams.set('code', searchCode);
|
||||||
else url.searchParams.delete('code');
|
else url.searchParams.delete('code');
|
||||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||||
else url.searchParams.delete('description');
|
else url.searchParams.delete('description');
|
||||||
history.replaceState(history.state, '', url);
|
history.replaceState(history.state, '', url);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMore() {
|
async function loadMore() {
|
||||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await identifiersApi.list(companyStore.activeCompany.id, {
|
const response = await identifiersApi.list(companyStore.activeCompany.id, {
|
||||||
code: searchCode || undefined,
|
code: searchCode || undefined,
|
||||||
description: searchDesc || undefined,
|
description: searchDesc || undefined,
|
||||||
page: (currentPage + 1).toString(),
|
page: (currentPage + 1).toString(),
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data?.items) {
|
if (response.data?.items) {
|
||||||
allItems = [...allItems, ...response.data.items];
|
allItems = [...allItems, ...response.data.items];
|
||||||
currentPage += 1;
|
currentPage += 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadData() {
|
async function reloadData() {
|
||||||
if (!companyStore.activeCompany) return;
|
if (!companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await identifiersApi.list(companyStore.activeCompany.id, {
|
const response = await identifiersApi.list(companyStore.activeCompany.id, {
|
||||||
code: searchCode || undefined,
|
code: searchCode || undefined,
|
||||||
description: searchDesc || undefined,
|
description: searchDesc || undefined,
|
||||||
page: '1',
|
page: '1',
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
allItems = response.data.items;
|
allItems = response.data.items;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSuccess() {
|
function handleSuccess() {
|
||||||
createDialogOpen = false;
|
createDialogOpen = false;
|
||||||
editingItem = null;
|
editingItem = null;
|
||||||
selectedIds = [];
|
selectedIds = [];
|
||||||
reloadData();
|
reloadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]">
|
<div
|
||||||
<div class="flex flex-none items-center justify-between">
|
class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]"
|
||||||
<div>
|
>
|
||||||
<h1 class="text-2xl font-bold tracking-tight">Identificadores</h1>
|
<div class="flex flex-none items-center justify-between">
|
||||||
<p class="text-muted-foreground">Catálogo de Identificadores del sistema</p>
|
<div>
|
||||||
</div>
|
<h1 class="text-2xl font-bold tracking-tight">Identificadores</h1>
|
||||||
<div class="flex items-center gap-3">
|
<p class="text-muted-foreground">Catálogo de Identificadores del sistema</p>
|
||||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
</div>
|
||||||
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
<div class="flex items-center gap-3">
|
||||||
</Button>
|
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||||
{#if !isError && canCreate}
|
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
||||||
<Button class="h-9" onclick={() => { editingItem = null; createDialogOpen = true; }}>
|
</Button>
|
||||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
{#if !isError && canCreate}
|
||||||
</Button>
|
<Button
|
||||||
{/if}
|
class="h-9"
|
||||||
</div>
|
onclick={() => {
|
||||||
</div>
|
editingItem = null;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if isError}
|
{#if isError}
|
||||||
<ErrorState
|
<ErrorState
|
||||||
status={!canView ? 403 : status}
|
status={!canView ? 403 : status}
|
||||||
error={!canView ? 'Permission denied: cat_identifiers.view' : error || ''}
|
error={!canView ? 'Permission denied: cat_identifiers.view' : error || ''}
|
||||||
onRetry={reloadData}
|
onRetry={reloadData}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<Card.Title>Listado de Identificadores</Card.Title>
|
<Card.Title>Listado de Identificadores</Card.Title>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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
|
||||||
<Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
placeholder="Código"
|
||||||
</div>
|
bind:value={searchCode}
|
||||||
</div>
|
oninput={handleSearch}
|
||||||
</Card.Header>
|
class="h-9 w-36 bg-card lg:w-44"
|
||||||
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
/>
|
||||||
<div class="h-full overflow-hidden rounded-md border bg-background">
|
<Input
|
||||||
<InfiniteDataTable
|
placeholder="Descripción"
|
||||||
data={allItems} {columns} {loading} {hasMore} {loadMore}
|
bind:value={searchDesc}
|
||||||
{selectedIds} onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
oninput={handleSearch}
|
||||||
onRowClick={(row) => selectedIds = selectedIds.includes(row.id) ? [] : [row.id]}
|
class="h-9 w-44 bg-card lg:w-64"
|
||||||
onRowDoubleClick={(row) => { if(canEdit) { editingItem = row; createDialogOpen = true; } }}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Header>
|
||||||
</Card.Root>
|
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
||||||
|
<div class="h-full overflow-hidden rounded-md border bg-background">
|
||||||
|
<InfiniteDataTable
|
||||||
|
data={allItems}
|
||||||
|
{columns}
|
||||||
|
{loading}
|
||||||
|
{hasMore}
|
||||||
|
{loadMore}
|
||||||
|
{selectedIds}
|
||||||
|
onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
||||||
|
onRowClick={(row) => (selectedIds = selectedIds.includes(row.id) ? [] : [row.id])}
|
||||||
|
onRowDoubleClick={(row) => {
|
||||||
|
if (canEdit) {
|
||||||
|
editingItem = row;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
<div class="flex-none text-sm text-muted-foreground">
|
<div class="flex-none text-sm text-muted-foreground">
|
||||||
Mostrando {allItems.length} de {totalItems} registros
|
Mostrando {allItems.length} de {totalItems} registros
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed right-0 bottom-0 left-0 z-50 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"
|
||||||
|
>
|
||||||
|
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
{#if canEdit}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => {
|
||||||
|
editingItem = selectedItem;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}}
|
||||||
|
disabled={!selectedItem}
|
||||||
|
>
|
||||||
|
<Pencil size={16} class="mr-2" /> Editar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if canDelete}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={async () => {
|
||||||
|
if (!selectedItem || !companyStore.activeCompany) return;
|
||||||
|
if (confirm('¿Eliminar este registro?')) {
|
||||||
|
await identifiersApi.delete(selectedItem.id, companyStore.activeCompany.id);
|
||||||
|
selectedIds = [];
|
||||||
|
reloadData();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!selectedItem}
|
||||||
|
class="text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} class="mr-2" /> Eliminar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="fixed right-0 bottom-0 left-0 z-50 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">
|
<CreateEditDialog bind:open={createDialogOpen} item={editingItem} onSuccess={handleSuccess} />
|
||||||
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
{#if canEdit}
|
|
||||||
<Button variant="outline" size="sm" onclick={() => { editingItem = selectedItem; createDialogOpen = true; }} disabled={!selectedItem}>
|
|
||||||
<Pencil size={16} class="mr-2" /> Editar
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{#if canDelete}
|
|
||||||
<Button variant="outline" size="sm" onclick={async () => {
|
|
||||||
if(!selectedItem || !companyStore.activeCompany) return;
|
|
||||||
if(confirm('¿Eliminar este registro?')) {
|
|
||||||
await identifiersApi.delete(selectedItem.id, companyStore.activeCompany.id);
|
|
||||||
selectedIds = [];
|
|
||||||
reloadData();
|
|
||||||
}
|
|
||||||
}} disabled={!selectedItem} class="text-destructive hover:bg-destructive/10">
|
|
||||||
<Trash2 size={16} class="mr-2" /> Eliminar
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<CreateEditDialog bind:open={createDialogOpen} item={editingItem} onSuccess={handleSuccess} />
|
|
||||||
</div>
|
|
||||||
@@ -1,237 +1,280 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/inpc/columns';
|
import { createColumns } from '$lib/components/dashboard/general_catalogs/inpc/columns';
|
||||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/inpc/create-edit-dialog.svelte';
|
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/inpc/create-edit-dialog.svelte';
|
||||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
||||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||||
import { obtenerAtajosListaINPC } from '$lib/config/shortcuts/dashboard/general_catalogs/inpc/list';
|
import { obtenerAtajosListaINPC } from '$lib/config/shortcuts/dashboard/general_catalogs/inpc/list';
|
||||||
import { inpcApi, type INPC } from '$lib/api/dashboard/a76/general_catalogs/inpc';
|
import { inpcApi, type INPC } from '$lib/api/dashboard/a76/general_catalogs/inpc';
|
||||||
import { companyStore } from '$lib/stores/company.svelte';
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
import { currentUser, userHasPermission } from '$lib/auth';
|
import { currentUser, userHasPermission } from '$lib/auth';
|
||||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let createDialogOpen = $state(false);
|
let createDialogOpen = $state(false);
|
||||||
let editingItem = $state<INPC | null>(null);
|
let editingItem = $state<INPC | null>(null);
|
||||||
let error = $state<string | null>(data.error || null);
|
let error = $state<string | null>(data.error || null);
|
||||||
let status = $state<number>(data.status || 200);
|
let status = $state<number>(data.status || 200);
|
||||||
|
|
||||||
// Permisos
|
// Permisos
|
||||||
const canView = $derived(userHasPermission($currentUser, 'cat_inpc.view'));
|
const canView = $derived(userHasPermission($currentUser, 'cat_inpc.view'));
|
||||||
const canCreate = $derived(userHasPermission($currentUser, 'cat_inpc.create'));
|
const canCreate = $derived(userHasPermission($currentUser, 'cat_inpc.create'));
|
||||||
const canEdit = $derived(userHasPermission($currentUser, 'cat_inpc.edit'));
|
const canEdit = $derived(userHasPermission($currentUser, 'cat_inpc.edit'));
|
||||||
const canDelete = $derived(userHasPermission($currentUser, 'cat_inpc.delete'));
|
const canDelete = $derived(userHasPermission($currentUser, 'cat_inpc.delete'));
|
||||||
|
|
||||||
const isError = $derived(!canView || status >= 400 || error);
|
const isError = $derived(!canView || status >= 400 || error);
|
||||||
|
|
||||||
// Atajos
|
// Atajos
|
||||||
useShortcuts(
|
useShortcuts(
|
||||||
'Lista INPC',
|
'Lista INPC',
|
||||||
obtenerAtajosListaINPC({
|
obtenerAtajosListaINPC({
|
||||||
manejarNuevo: () => {
|
manejarNuevo: () => {
|
||||||
if (!canCreate) return;
|
if (!canCreate) return;
|
||||||
editingItem = null;
|
editingItem = null;
|
||||||
createDialogOpen = true;
|
createDialogOpen = true;
|
||||||
},
|
},
|
||||||
manejarActualizar: () => reloadData()
|
manejarActualizar: () => reloadData()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filtros
|
// Filtros
|
||||||
let searchYear = $state($page.url.searchParams.get('year') || '');
|
let searchYear = $state($page.url.searchParams.get('year') || '');
|
||||||
let searchMonth = $state($page.url.searchParams.get('month') || '');
|
let searchMonth = $state($page.url.searchParams.get('month') || '');
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
let allItems = $state<INPC[]>(data.items?.items || data.items || []);
|
let allItems = $state<INPC[]>(data.inpc?.items || data.inpc || []);
|
||||||
let currentPage = $state(data.items?.page || 1);
|
let currentPage = $state(data.inpc?.page || 1);
|
||||||
let pageSize = $state(data.items?.page_size || 50);
|
let pageSize = $state(data.inpc?.page_size || 50);
|
||||||
let totalItems = $state(data.items?.total || 0);
|
let totalItems = $state(data.inpc?.total || 0);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let hasMore = $derived(allItems.length < totalItems);
|
let hasMore = $derived(allItems.length < totalItems);
|
||||||
let selectedIds = $state<(string | number)[]>([]);
|
let selectedIds = $state<(string | number)[]>([]);
|
||||||
const selectedItem = $derived(
|
const selectedItem = $derived(
|
||||||
selectedIds.length === 1
|
selectedIds.length === 1
|
||||||
? allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null
|
? (allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (data.items) {
|
if (data.inpc) {
|
||||||
allItems = data.items.items || data.items || [];
|
allItems = data.inpc.items || data.inpc || [];
|
||||||
currentPage = data.items.page || 1;
|
currentPage = data.inpc.page || 1;
|
||||||
totalItems = data.items.total || 0;
|
totalItems = data.inpc.total || 0;
|
||||||
pageSize = data.items.page_size || pageSize;
|
pageSize = data.inpc.page_size || pageSize;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleSearch() {
|
async function handleSearch() {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(async () => {
|
timeout = setTimeout(async () => {
|
||||||
if (!companyStore.activeCompany) return;
|
if (!companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await inpcApi.list(companyStore.activeCompany.id, {
|
const response = await inpcApi.list(companyStore.activeCompany.id, {
|
||||||
year: searchYear || undefined,
|
year: searchYear || undefined,
|
||||||
month: searchMonth || undefined,
|
month: searchMonth || undefined,
|
||||||
page: '1',
|
page: '1',
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
allItems = response.data.items;
|
allItems = response.data.items;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Error aplicando filtros';
|
error = 'Error aplicando filtros';
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL($page.url);
|
const url = new URL($page.url);
|
||||||
if (searchYear) url.searchParams.set('year', searchYear);
|
if (searchYear) url.searchParams.set('year', searchYear);
|
||||||
else url.searchParams.delete('year');
|
else url.searchParams.delete('year');
|
||||||
if (searchMonth) url.searchParams.set('month', searchMonth);
|
if (searchMonth) url.searchParams.set('month', searchMonth);
|
||||||
else url.searchParams.delete('month');
|
else url.searchParams.delete('month');
|
||||||
history.replaceState(history.state, '', url);
|
history.replaceState(history.state, '', url);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMore() {
|
async function loadMore() {
|
||||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await inpcApi.list(companyStore.activeCompany.id, {
|
const response = await inpcApi.list(companyStore.activeCompany.id, {
|
||||||
year: searchYear || undefined,
|
year: searchYear || undefined,
|
||||||
month: searchMonth || undefined,
|
month: searchMonth || undefined,
|
||||||
page: (currentPage + 1).toString(),
|
page: (currentPage + 1).toString(),
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data?.items) {
|
if (response.data?.items) {
|
||||||
allItems = [...allItems, ...response.data.items];
|
allItems = [...allItems, ...response.data.items];
|
||||||
currentPage += 1;
|
currentPage += 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadData() {
|
async function reloadData() {
|
||||||
if (!companyStore.activeCompany) return;
|
if (!companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await inpcApi.list(companyStore.activeCompany.id, {
|
const response = await inpcApi.list(companyStore.activeCompany.id, {
|
||||||
year: searchYear || undefined,
|
year: searchYear || undefined,
|
||||||
month: searchMonth || undefined,
|
month: searchMonth || undefined,
|
||||||
page: '1',
|
page: '1',
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
allItems = response.data.items;
|
allItems = response.data.items;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSuccess() {
|
function handleSuccess() {
|
||||||
createDialogOpen = false;
|
createDialogOpen = false;
|
||||||
editingItem = null;
|
editingItem = null;
|
||||||
selectedIds = [];
|
selectedIds = [];
|
||||||
reloadData();
|
reloadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]">
|
<div
|
||||||
<div class="flex flex-none items-center justify-between">
|
class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]"
|
||||||
<div>
|
>
|
||||||
<h1 class="text-2xl font-bold tracking-tight">INPC</h1>
|
<div class="flex flex-none items-center justify-between">
|
||||||
<p class="text-muted-foreground">Catálogo de INPC del sistema</p>
|
<div>
|
||||||
</div>
|
<h1 class="text-2xl font-bold tracking-tight">INPC</h1>
|
||||||
<div class="flex items-center gap-3">
|
<p class="text-muted-foreground">Catálogo de INPC del sistema</p>
|
||||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
</div>
|
||||||
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
<div class="flex items-center gap-3">
|
||||||
</Button>
|
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||||
{#if !isError && canCreate}
|
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
||||||
<Button class="h-9" onclick={() => { editingItem = null; createDialogOpen = true; }}>
|
</Button>
|
||||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
{#if !isError && canCreate}
|
||||||
</Button>
|
<Button
|
||||||
{/if}
|
class="h-9"
|
||||||
</div>
|
onclick={() => {
|
||||||
</div>
|
editingItem = null;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if isError}
|
{#if isError}
|
||||||
<ErrorState
|
<ErrorState
|
||||||
status={!canView ? 403 : status}
|
status={!canView ? 403 : status}
|
||||||
error={!canView ? 'Permission denied: cat_inpc.view' : error || ''}
|
error={!canView ? 'Permission denied: cat_inpc.view' : error || ''}
|
||||||
onRetry={reloadData}
|
onRetry={reloadData}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<Card.Title>Listado de INPC</Card.Title>
|
<Card.Title>Listado de INPC</Card.Title>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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
|
||||||
<Input placeholder="Mes" bind:value={searchMonth} oninput={handleSearch} class="h-9 w-28 bg-card lg:w-36" />
|
placeholder="Año"
|
||||||
</div>
|
bind:value={searchYear}
|
||||||
</div>
|
oninput={handleSearch}
|
||||||
</Card.Header>
|
class="h-9 w-28 bg-card lg:w-36"
|
||||||
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
/>
|
||||||
<div class="h-full overflow-hidden rounded-md border bg-background">
|
<Input
|
||||||
<InfiniteDataTable
|
placeholder="Mes"
|
||||||
data={allItems} {columns} {loading} {hasMore} {loadMore}
|
bind:value={searchMonth}
|
||||||
{selectedIds} onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
oninput={handleSearch}
|
||||||
onRowClick={(row) => selectedIds = selectedIds.includes(row.id) ? [] : [row.id]}
|
class="h-9 w-28 bg-card lg:w-36"
|
||||||
onRowDoubleClick={(row) => { if(canEdit) { editingItem = row; createDialogOpen = true; } }}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Header>
|
||||||
</Card.Root>
|
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
||||||
|
<div class="h-full overflow-hidden rounded-md border bg-background">
|
||||||
|
<InfiniteDataTable
|
||||||
|
data={allItems}
|
||||||
|
{columns}
|
||||||
|
{loading}
|
||||||
|
{hasMore}
|
||||||
|
{loadMore}
|
||||||
|
{selectedIds}
|
||||||
|
onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
||||||
|
onRowClick={(row) => (selectedIds = selectedIds.includes(row.id) ? [] : [row.id])}
|
||||||
|
onRowDoubleClick={(row) => {
|
||||||
|
if (canEdit) {
|
||||||
|
editingItem = row;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
<div class="flex-none text-sm text-muted-foreground">
|
<div class="flex-none text-sm text-muted-foreground">
|
||||||
Mostrando {allItems.length} de {totalItems} registros
|
Mostrando {allItems.length} de {totalItems} registros
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed right-0 bottom-0 left-0 z-50 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"
|
||||||
|
>
|
||||||
|
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
{#if canEdit}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => {
|
||||||
|
editingItem = selectedItem;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}}
|
||||||
|
disabled={!selectedItem}
|
||||||
|
>
|
||||||
|
<Pencil size={16} class="mr-2" /> Editar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if canDelete}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={async () => {
|
||||||
|
if (!selectedItem || !companyStore.activeCompany) return;
|
||||||
|
if (confirm('¿Eliminar este registro?')) {
|
||||||
|
await inpcApi.delete(selectedItem.id, companyStore.activeCompany.id);
|
||||||
|
selectedIds = [];
|
||||||
|
reloadData();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!selectedItem}
|
||||||
|
class="text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} class="mr-2" /> Eliminar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="fixed right-0 bottom-0 left-0 z-50 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">
|
<CreateEditDialog bind:open={createDialogOpen} item={editingItem} onSuccess={handleSuccess} />
|
||||||
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
{#if canEdit}
|
|
||||||
<Button variant="outline" size="sm" onclick={() => { editingItem = selectedItem; createDialogOpen = true; }} disabled={!selectedItem}>
|
|
||||||
<Pencil size={16} class="mr-2" /> Editar
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{#if canDelete}
|
|
||||||
<Button variant="outline" size="sm" onclick={async () => {
|
|
||||||
if(!selectedItem || !companyStore.activeCompany) return;
|
|
||||||
if(confirm('¿Eliminar este registro?')) {
|
|
||||||
await inpcApi.delete(selectedItem.id, companyStore.activeCompany.id);
|
|
||||||
selectedIds = [];
|
|
||||||
reloadData();
|
|
||||||
}
|
|
||||||
}} disabled={!selectedItem} class="text-destructive hover:bg-destructive/10">
|
|
||||||
<Trash2 size={16} class="mr-2" /> Eliminar
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<CreateEditDialog bind:open={createDialogOpen} item={editingItem} onSuccess={handleSuccess} />
|
|
||||||
</div>
|
|
||||||
@@ -1,237 +1,280 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/legends/columns';
|
import { createColumns } from '$lib/components/dashboard/general_catalogs/legends/columns';
|
||||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/legends/create-edit-dialog.svelte';
|
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/legends/create-edit-dialog.svelte';
|
||||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
||||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||||
import { obtenerAtajosListaLeyendas } from '$lib/config/shortcuts/dashboard/general_catalogs/legends/list';
|
import { obtenerAtajosListaLeyendas } from '$lib/config/shortcuts/dashboard/general_catalogs/legends/list';
|
||||||
import { legendsApi, type Legend } from '$lib/api/dashboard/a76/general_catalogs/legends';
|
import { legendsApi, type Legend } from '$lib/api/dashboard/a76/general_catalogs/legends';
|
||||||
import { companyStore } from '$lib/stores/company.svelte';
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
import { currentUser, userHasPermission } from '$lib/auth';
|
import { currentUser, userHasPermission } from '$lib/auth';
|
||||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let createDialogOpen = $state(false);
|
let createDialogOpen = $state(false);
|
||||||
let editingItem = $state<Legend | null>(null);
|
let editingItem = $state<Legend | null>(null);
|
||||||
let error = $state<string | null>(data.error || null);
|
let error = $state<string | null>(data.error || null);
|
||||||
let status = $state<number>(data.status || 200);
|
let status = $state<number>(data.status || 200);
|
||||||
|
|
||||||
// Permisos
|
// Permisos
|
||||||
const canView = $derived(userHasPermission($currentUser, 'cat_legends.view'));
|
const canView = $derived(userHasPermission($currentUser, 'cat_legends.view'));
|
||||||
const canCreate = $derived(userHasPermission($currentUser, 'cat_legends.create'));
|
const canCreate = $derived(userHasPermission($currentUser, 'cat_legends.create'));
|
||||||
const canEdit = $derived(userHasPermission($currentUser, 'cat_legends.edit'));
|
const canEdit = $derived(userHasPermission($currentUser, 'cat_legends.edit'));
|
||||||
const canDelete = $derived(userHasPermission($currentUser, 'cat_legends.delete'));
|
const canDelete = $derived(userHasPermission($currentUser, 'cat_legends.delete'));
|
||||||
|
|
||||||
const isError = $derived(!canView || status >= 400 || error);
|
const isError = $derived(!canView || status >= 400 || error);
|
||||||
|
|
||||||
// Atajos
|
// Atajos
|
||||||
useShortcuts(
|
useShortcuts(
|
||||||
'Lista Leyendas',
|
'Lista Leyendas',
|
||||||
obtenerAtajosListaLeyendas({
|
obtenerAtajosListaLeyendas({
|
||||||
manejarNuevo: () => {
|
manejarNuevo: () => {
|
||||||
if (!canCreate) return;
|
if (!canCreate) return;
|
||||||
editingItem = null;
|
editingItem = null;
|
||||||
createDialogOpen = true;
|
createDialogOpen = true;
|
||||||
},
|
},
|
||||||
manejarActualizar: () => reloadData()
|
manejarActualizar: () => reloadData()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filtros
|
// Filtros
|
||||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
let allItems = $state<Legend[]>(data.items?.items || data.items || []);
|
let allItems = $state<Legend[]>(data.legends?.items || data.legends || []);
|
||||||
let currentPage = $state(data.items?.page || 1);
|
let currentPage = $state(data.legends?.page || 1);
|
||||||
let pageSize = $state(data.items?.page_size || 50);
|
let pageSize = $state(data.legends?.page_size || 50);
|
||||||
let totalItems = $state(data.items?.total || 0);
|
let totalItems = $state(data.legends?.total || 0);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let hasMore = $derived(allItems.length < totalItems);
|
let hasMore = $derived(allItems.length < totalItems);
|
||||||
let selectedIds = $state<(string | number)[]>([]);
|
let selectedIds = $state<(string | number)[]>([]);
|
||||||
const selectedItem = $derived(
|
const selectedItem = $derived(
|
||||||
selectedIds.length === 1
|
selectedIds.length === 1
|
||||||
? allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null
|
? (allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (data.items) {
|
if (data.legends) {
|
||||||
allItems = data.items.items || data.items || [];
|
allItems = data.legends.items || data.legends || [];
|
||||||
currentPage = data.items.page || 1;
|
currentPage = data.legends.page || 1;
|
||||||
totalItems = data.items.total || 0;
|
totalItems = data.legends.total || 0;
|
||||||
pageSize = data.items.page_size || pageSize;
|
pageSize = data.legends.page_size || pageSize;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleSearch() {
|
async function handleSearch() {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(async () => {
|
timeout = setTimeout(async () => {
|
||||||
if (!companyStore.activeCompany) return;
|
if (!companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await legendsApi.list(companyStore.activeCompany.id, {
|
const response = await legendsApi.list(companyStore.activeCompany.id, {
|
||||||
code: searchCode || undefined,
|
code: searchCode || undefined,
|
||||||
description: searchDesc || undefined,
|
description: searchDesc || undefined,
|
||||||
page: '1',
|
page: '1',
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
allItems = response.data.items;
|
allItems = response.data.items;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Error aplicando filtros';
|
error = 'Error aplicando filtros';
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL($page.url);
|
const url = new URL($page.url);
|
||||||
if (searchCode) url.searchParams.set('code', searchCode);
|
if (searchCode) url.searchParams.set('code', searchCode);
|
||||||
else url.searchParams.delete('code');
|
else url.searchParams.delete('code');
|
||||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||||
else url.searchParams.delete('description');
|
else url.searchParams.delete('description');
|
||||||
history.replaceState(history.state, '', url);
|
history.replaceState(history.state, '', url);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMore() {
|
async function loadMore() {
|
||||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await legendsApi.list(companyStore.activeCompany.id, {
|
const response = await legendsApi.list(companyStore.activeCompany.id, {
|
||||||
code: searchCode || undefined,
|
code: searchCode || undefined,
|
||||||
description: searchDesc || undefined,
|
description: searchDesc || undefined,
|
||||||
page: (currentPage + 1).toString(),
|
page: (currentPage + 1).toString(),
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data?.items) {
|
if (response.data?.items) {
|
||||||
allItems = [...allItems, ...response.data.items];
|
allItems = [...allItems, ...response.data.items];
|
||||||
currentPage += 1;
|
currentPage += 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadData() {
|
async function reloadData() {
|
||||||
if (!companyStore.activeCompany) return;
|
if (!companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await legendsApi.list(companyStore.activeCompany.id, {
|
const response = await legendsApi.list(companyStore.activeCompany.id, {
|
||||||
code: searchCode || undefined,
|
code: searchCode || undefined,
|
||||||
description: searchDesc || undefined,
|
description: searchDesc || undefined,
|
||||||
page: '1',
|
page: '1',
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
allItems = response.data.items;
|
allItems = response.data.items;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSuccess() {
|
function handleSuccess() {
|
||||||
createDialogOpen = false;
|
createDialogOpen = false;
|
||||||
editingItem = null;
|
editingItem = null;
|
||||||
selectedIds = [];
|
selectedIds = [];
|
||||||
reloadData();
|
reloadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]">
|
<div
|
||||||
<div class="flex flex-none items-center justify-between">
|
class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]"
|
||||||
<div>
|
>
|
||||||
<h1 class="text-2xl font-bold tracking-tight">Leyendas</h1>
|
<div class="flex flex-none items-center justify-between">
|
||||||
<p class="text-muted-foreground">Catálogo de Leyendas del sistema</p>
|
<div>
|
||||||
</div>
|
<h1 class="text-2xl font-bold tracking-tight">Leyendas</h1>
|
||||||
<div class="flex items-center gap-3">
|
<p class="text-muted-foreground">Catálogo de Leyendas del sistema</p>
|
||||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
</div>
|
||||||
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
<div class="flex items-center gap-3">
|
||||||
</Button>
|
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||||
{#if !isError && canCreate}
|
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
||||||
<Button class="h-9" onclick={() => { editingItem = null; createDialogOpen = true; }}>
|
</Button>
|
||||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
{#if !isError && canCreate}
|
||||||
</Button>
|
<Button
|
||||||
{/if}
|
class="h-9"
|
||||||
</div>
|
onclick={() => {
|
||||||
</div>
|
editingItem = null;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if isError}
|
{#if isError}
|
||||||
<ErrorState
|
<ErrorState
|
||||||
status={!canView ? 403 : status}
|
status={!canView ? 403 : status}
|
||||||
error={!canView ? 'Permission denied: cat_legends.view' : error || ''}
|
error={!canView ? 'Permission denied: cat_legends.view' : error || ''}
|
||||||
onRetry={reloadData}
|
onRetry={reloadData}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<Card.Title>Listado de Leyendas</Card.Title>
|
<Card.Title>Listado de Leyendas</Card.Title>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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
|
||||||
<Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
placeholder="Código"
|
||||||
</div>
|
bind:value={searchCode}
|
||||||
</div>
|
oninput={handleSearch}
|
||||||
</Card.Header>
|
class="h-9 w-36 bg-card lg:w-44"
|
||||||
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
/>
|
||||||
<div class="h-full overflow-hidden rounded-md border bg-background">
|
<Input
|
||||||
<InfiniteDataTable
|
placeholder="Descripción"
|
||||||
data={allItems} {columns} {loading} {hasMore} {loadMore}
|
bind:value={searchDesc}
|
||||||
{selectedIds} onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
oninput={handleSearch}
|
||||||
onRowClick={(row) => selectedIds = selectedIds.includes(row.id) ? [] : [row.id]}
|
class="h-9 w-44 bg-card lg:w-64"
|
||||||
onRowDoubleClick={(row) => { if(canEdit) { editingItem = row; createDialogOpen = true; } }}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Header>
|
||||||
</Card.Root>
|
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
||||||
|
<div class="h-full overflow-hidden rounded-md border bg-background">
|
||||||
|
<InfiniteDataTable
|
||||||
|
data={allItems}
|
||||||
|
{columns}
|
||||||
|
{loading}
|
||||||
|
{hasMore}
|
||||||
|
{loadMore}
|
||||||
|
{selectedIds}
|
||||||
|
onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
||||||
|
onRowClick={(row) => (selectedIds = selectedIds.includes(row.id) ? [] : [row.id])}
|
||||||
|
onRowDoubleClick={(row) => {
|
||||||
|
if (canEdit) {
|
||||||
|
editingItem = row;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
<div class="flex-none text-sm text-muted-foreground">
|
<div class="flex-none text-sm text-muted-foreground">
|
||||||
Mostrando {allItems.length} de {totalItems} registros
|
Mostrando {allItems.length} de {totalItems} registros
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed right-0 bottom-0 left-0 z-50 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"
|
||||||
|
>
|
||||||
|
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
{#if canEdit}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => {
|
||||||
|
editingItem = selectedItem;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}}
|
||||||
|
disabled={!selectedItem}
|
||||||
|
>
|
||||||
|
<Pencil size={16} class="mr-2" /> Editar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if canDelete}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={async () => {
|
||||||
|
if (!selectedItem || !companyStore.activeCompany) return;
|
||||||
|
if (confirm('¿Eliminar este registro?')) {
|
||||||
|
await legendsApi.delete(selectedItem.id, companyStore.activeCompany.id);
|
||||||
|
selectedIds = [];
|
||||||
|
reloadData();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!selectedItem}
|
||||||
|
class="text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} class="mr-2" /> Eliminar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="fixed right-0 bottom-0 left-0 z-50 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">
|
<CreateEditDialog bind:open={createDialogOpen} item={editingItem} onSuccess={handleSuccess} />
|
||||||
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
{#if canEdit}
|
|
||||||
<Button variant="outline" size="sm" onclick={() => { editingItem = selectedItem; createDialogOpen = true; }} disabled={!selectedItem}>
|
|
||||||
<Pencil size={16} class="mr-2" /> Editar
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{#if canDelete}
|
|
||||||
<Button variant="outline" size="sm" onclick={async () => {
|
|
||||||
if(!selectedItem || !companyStore.activeCompany) return;
|
|
||||||
if(confirm('¿Eliminar este registro?')) {
|
|
||||||
await legendsApi.delete(selectedItem.id, companyStore.activeCompany.id);
|
|
||||||
selectedIds = [];
|
|
||||||
reloadData();
|
|
||||||
}
|
|
||||||
}} disabled={!selectedItem} class="text-destructive hover:bg-destructive/10">
|
|
||||||
<Trash2 size={16} class="mr-2" /> Eliminar
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<CreateEditDialog bind:open={createDialogOpen} item={editingItem} onSuccess={handleSuccess} />
|
|
||||||
</div>
|
|
||||||
@@ -1,237 +1,280 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { createColumns } from '$lib/components/dashboard/ports/columns';
|
import { createColumns } from '$lib/components/dashboard/ports/columns';
|
||||||
import CreateEditDialog from '$lib/components/dashboard/ports/create-edit-dialog.svelte';
|
import CreateEditDialog from '$lib/components/dashboard/ports/create-edit-dialog.svelte';
|
||||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
||||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||||
import { obtenerAtajosListaPuertos } from '$lib/config/shortcuts/dashboard/general_catalogs/ports/list';
|
import { obtenerAtajosListaPuertos } from '$lib/config/shortcuts/dashboard/general_catalogs/ports/list';
|
||||||
import { portsApi, type Port } from '$lib/api/dashboard/a76/general_catalogs/ports';
|
import { portsApi, type Port } from '$lib/api/dashboard/a76/general_catalogs/ports';
|
||||||
import { companyStore } from '$lib/stores/company.svelte';
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
import { currentUser, userHasPermission } from '$lib/auth';
|
import { currentUser, userHasPermission } from '$lib/auth';
|
||||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let createDialogOpen = $state(false);
|
let createDialogOpen = $state(false);
|
||||||
let editingItem = $state<Port | null>(null);
|
let editingItem = $state<Port | null>(null);
|
||||||
let error = $state<string | null>(data.error || null);
|
let error = $state<string | null>(data.error || null);
|
||||||
let status = $state<number>(data.status || 200);
|
let status = $state<number>(data.status || 200);
|
||||||
|
|
||||||
// Permisos
|
// Permisos
|
||||||
const canView = $derived(userHasPermission($currentUser, 'cat_ports.view'));
|
const canView = $derived(userHasPermission($currentUser, 'cat_ports.view'));
|
||||||
const canCreate = $derived(userHasPermission($currentUser, 'cat_ports.create'));
|
const canCreate = $derived(userHasPermission($currentUser, 'cat_ports.create'));
|
||||||
const canEdit = $derived(userHasPermission($currentUser, 'cat_ports.edit'));
|
const canEdit = $derived(userHasPermission($currentUser, 'cat_ports.edit'));
|
||||||
const canDelete = $derived(userHasPermission($currentUser, 'cat_ports.delete'));
|
const canDelete = $derived(userHasPermission($currentUser, 'cat_ports.delete'));
|
||||||
|
|
||||||
const isError = $derived(!canView || status >= 400 || error);
|
const isError = $derived(!canView || status >= 400 || error);
|
||||||
|
|
||||||
// Atajos
|
// Atajos
|
||||||
useShortcuts(
|
useShortcuts(
|
||||||
'Lista Puertos',
|
'Lista Puertos',
|
||||||
obtenerAtajosListaPuertos({
|
obtenerAtajosListaPuertos({
|
||||||
manejarNuevo: () => {
|
manejarNuevo: () => {
|
||||||
if (!canCreate) return;
|
if (!canCreate) return;
|
||||||
editingItem = null;
|
editingItem = null;
|
||||||
createDialogOpen = true;
|
createDialogOpen = true;
|
||||||
},
|
},
|
||||||
manejarActualizar: () => reloadData()
|
manejarActualizar: () => reloadData()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filtros
|
// Filtros
|
||||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
let allItems = $state<Port[]>(data.items?.items || data.items || []);
|
let allItems = $state<Port[]>(data.ports?.items || data.ports || []);
|
||||||
let currentPage = $state(data.items?.page || 1);
|
let currentPage = $state(data.ports?.page || 1);
|
||||||
let pageSize = $state(data.items?.page_size || 50);
|
let pageSize = $state(data.ports?.page_size || 50);
|
||||||
let totalItems = $state(data.items?.total || 0);
|
let totalItems = $state(data.ports?.total || 0);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let hasMore = $derived(allItems.length < totalItems);
|
let hasMore = $derived(allItems.length < totalItems);
|
||||||
let selectedIds = $state<(string | number)[]>([]);
|
let selectedIds = $state<(string | number)[]>([]);
|
||||||
const selectedItem = $derived(
|
const selectedItem = $derived(
|
||||||
selectedIds.length === 1
|
selectedIds.length === 1
|
||||||
? allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null
|
? (allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (data.items) {
|
if (data.ports) {
|
||||||
allItems = data.items.items || data.items || [];
|
allItems = data.ports.items || data.ports || [];
|
||||||
currentPage = data.items.page || 1;
|
currentPage = data.ports.page || 1;
|
||||||
totalItems = data.items.total || 0;
|
totalItems = data.ports.total || 0;
|
||||||
pageSize = data.items.page_size || pageSize;
|
pageSize = data.ports.page_size || pageSize;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleSearch() {
|
async function handleSearch() {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(async () => {
|
timeout = setTimeout(async () => {
|
||||||
if (!companyStore.activeCompany) return;
|
if (!companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await portsApi.list(companyStore.activeCompany.id, {
|
const response = await portsApi.list(companyStore.activeCompany.id, {
|
||||||
code: searchCode || undefined,
|
code: searchCode || undefined,
|
||||||
description: searchDesc || undefined,
|
description: searchDesc || undefined,
|
||||||
page: '1',
|
page: '1',
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
allItems = response.data.items;
|
allItems = response.data.items;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Error aplicando filtros';
|
error = 'Error aplicando filtros';
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL($page.url);
|
const url = new URL($page.url);
|
||||||
if (searchCode) url.searchParams.set('code', searchCode);
|
if (searchCode) url.searchParams.set('code', searchCode);
|
||||||
else url.searchParams.delete('code');
|
else url.searchParams.delete('code');
|
||||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||||
else url.searchParams.delete('description');
|
else url.searchParams.delete('description');
|
||||||
history.replaceState(history.state, '', url);
|
history.replaceState(history.state, '', url);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMore() {
|
async function loadMore() {
|
||||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await portsApi.list(companyStore.activeCompany.id, {
|
const response = await portsApi.list(companyStore.activeCompany.id, {
|
||||||
code: searchCode || undefined,
|
code: searchCode || undefined,
|
||||||
description: searchDesc || undefined,
|
description: searchDesc || undefined,
|
||||||
page: (currentPage + 1).toString(),
|
page: (currentPage + 1).toString(),
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data?.items) {
|
if (response.data?.items) {
|
||||||
allItems = [...allItems, ...response.data.items];
|
allItems = [...allItems, ...response.data.items];
|
||||||
currentPage += 1;
|
currentPage += 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadData() {
|
async function reloadData() {
|
||||||
if (!companyStore.activeCompany) return;
|
if (!companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await portsApi.list(companyStore.activeCompany.id, {
|
const response = await portsApi.list(companyStore.activeCompany.id, {
|
||||||
code: searchCode || undefined,
|
code: searchCode || undefined,
|
||||||
description: searchDesc || undefined,
|
description: searchDesc || undefined,
|
||||||
page: '1',
|
page: '1',
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
allItems = response.data.items;
|
allItems = response.data.items;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSuccess() {
|
function handleSuccess() {
|
||||||
createDialogOpen = false;
|
createDialogOpen = false;
|
||||||
editingItem = null;
|
editingItem = null;
|
||||||
selectedIds = [];
|
selectedIds = [];
|
||||||
reloadData();
|
reloadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]">
|
<div
|
||||||
<div class="flex flex-none items-center justify-between">
|
class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]"
|
||||||
<div>
|
>
|
||||||
<h1 class="text-2xl font-bold tracking-tight">Puertos</h1>
|
<div class="flex flex-none items-center justify-between">
|
||||||
<p class="text-muted-foreground">Catálogo de Puertos del sistema</p>
|
<div>
|
||||||
</div>
|
<h1 class="text-2xl font-bold tracking-tight">Puertos</h1>
|
||||||
<div class="flex items-center gap-3">
|
<p class="text-muted-foreground">Catálogo de Puertos del sistema</p>
|
||||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
</div>
|
||||||
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
<div class="flex items-center gap-3">
|
||||||
</Button>
|
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||||
{#if !isError && canCreate}
|
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
||||||
<Button class="h-9" onclick={() => { editingItem = null; createDialogOpen = true; }}>
|
</Button>
|
||||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
{#if !isError && canCreate}
|
||||||
</Button>
|
<Button
|
||||||
{/if}
|
class="h-9"
|
||||||
</div>
|
onclick={() => {
|
||||||
</div>
|
editingItem = null;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if isError}
|
{#if isError}
|
||||||
<ErrorState
|
<ErrorState
|
||||||
status={!canView ? 403 : status}
|
status={!canView ? 403 : status}
|
||||||
error={!canView ? 'Permission denied: cat_ports.view' : error || ''}
|
error={!canView ? 'Permission denied: cat_ports.view' : error || ''}
|
||||||
onRetry={reloadData}
|
onRetry={reloadData}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<Card.Title>Listado de Puertos</Card.Title>
|
<Card.Title>Listado de Puertos</Card.Title>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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
|
||||||
<Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
placeholder="Código"
|
||||||
</div>
|
bind:value={searchCode}
|
||||||
</div>
|
oninput={handleSearch}
|
||||||
</Card.Header>
|
class="h-9 w-36 bg-card lg:w-44"
|
||||||
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
/>
|
||||||
<div class="h-full overflow-hidden rounded-md border bg-background">
|
<Input
|
||||||
<InfiniteDataTable
|
placeholder="Descripción"
|
||||||
data={allItems} {columns} {loading} {hasMore} {loadMore}
|
bind:value={searchDesc}
|
||||||
{selectedIds} onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
oninput={handleSearch}
|
||||||
onRowClick={(row) => selectedIds = selectedIds.includes(row.id) ? [] : [row.id]}
|
class="h-9 w-44 bg-card lg:w-64"
|
||||||
onRowDoubleClick={(row) => { if(canEdit) { editingItem = row; createDialogOpen = true; } }}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Header>
|
||||||
</Card.Root>
|
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
||||||
|
<div class="h-full overflow-hidden rounded-md border bg-background">
|
||||||
|
<InfiniteDataTable
|
||||||
|
data={allItems}
|
||||||
|
{columns}
|
||||||
|
{loading}
|
||||||
|
{hasMore}
|
||||||
|
{loadMore}
|
||||||
|
{selectedIds}
|
||||||
|
onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
||||||
|
onRowClick={(row) => (selectedIds = selectedIds.includes(row.id) ? [] : [row.id])}
|
||||||
|
onRowDoubleClick={(row) => {
|
||||||
|
if (canEdit) {
|
||||||
|
editingItem = row;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
<div class="flex-none text-sm text-muted-foreground">
|
<div class="flex-none text-sm text-muted-foreground">
|
||||||
Mostrando {allItems.length} de {totalItems} registros
|
Mostrando {allItems.length} de {totalItems} registros
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed right-0 bottom-0 left-0 z-50 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"
|
||||||
|
>
|
||||||
|
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
{#if canEdit}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => {
|
||||||
|
editingItem = selectedItem;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}}
|
||||||
|
disabled={!selectedItem}
|
||||||
|
>
|
||||||
|
<Pencil size={16} class="mr-2" /> Editar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if canDelete}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={async () => {
|
||||||
|
if (!selectedItem || !companyStore.activeCompany) return;
|
||||||
|
if (confirm('¿Eliminar este registro?')) {
|
||||||
|
await portsApi.delete(selectedItem.id, companyStore.activeCompany.id);
|
||||||
|
selectedIds = [];
|
||||||
|
reloadData();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!selectedItem}
|
||||||
|
class="text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} class="mr-2" /> Eliminar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="fixed right-0 bottom-0 left-0 z-50 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">
|
<CreateEditDialog bind:open={createDialogOpen} item={editingItem} onSuccess={handleSuccess} />
|
||||||
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
{#if canEdit}
|
|
||||||
<Button variant="outline" size="sm" onclick={() => { editingItem = selectedItem; createDialogOpen = true; }} disabled={!selectedItem}>
|
|
||||||
<Pencil size={16} class="mr-2" /> Editar
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{#if canDelete}
|
|
||||||
<Button variant="outline" size="sm" onclick={async () => {
|
|
||||||
if(!selectedItem || !companyStore.activeCompany) return;
|
|
||||||
if(confirm('¿Eliminar este registro?')) {
|
|
||||||
await portsApi.delete(selectedItem.id, companyStore.activeCompany.id);
|
|
||||||
selectedIds = [];
|
|
||||||
reloadData();
|
|
||||||
}
|
|
||||||
}} disabled={!selectedItem} class="text-destructive hover:bg-destructive/10">
|
|
||||||
<Trash2 size={16} class="mr-2" /> Eliminar
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<CreateEditDialog bind:open={createDialogOpen} item={editingItem} onSuccess={handleSuccess} />
|
|
||||||
</div>
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||||
|
const parentData = await parent();
|
||||||
|
const { accessToken } = getAuthTokens(cookies);
|
||||||
|
if (!accessToken) {
|
||||||
|
throw redirect(302, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = Number(url.searchParams.get('page')) || 1;
|
||||||
|
const pageSize = Number(url.searchParams.get('page_size')) || 50;
|
||||||
|
const sealFilter = url.searchParams.get('seal')?.trim() || '';
|
||||||
|
|
||||||
|
const cookieCompanyId = cookies.get('active_company_id');
|
||||||
|
const companyId = cookieCompanyId
|
||||||
|
? parseInt(cookieCompanyId)
|
||||||
|
: parentData.companies?.[0]?.id;
|
||||||
|
|
||||||
|
if (!companyId) {
|
||||||
|
return {
|
||||||
|
seals: { items: [], total: 0, page, page_size: pageSize, pages: 0 },
|
||||||
|
error: 'Selecciona una compañía para ver los sellos',
|
||||||
|
status: 400
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiUrl = getServerApiUrl();
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
page: page.toString(),
|
||||||
|
page_size: pageSize.toString(),
|
||||||
|
company_id: companyId.toString()
|
||||||
|
});
|
||||||
|
if (sealFilter) {
|
||||||
|
query.set('seal', sealFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = `${apiUrl}v1/a76/seals/?${query.toString()}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMessage = `Error: ${response.status} ${response.statusText}`;
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
errorMessage = errorData.detail || errorData.message || errorMessage;
|
||||||
|
} catch {
|
||||||
|
// not JSON
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
seals: { items: [], total: 0, page, page_size: pageSize, pages: 0 },
|
||||||
|
error: errorMessage,
|
||||||
|
status: response.status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
seals: {
|
||||||
|
items: data.items,
|
||||||
|
total: data.total,
|
||||||
|
page: data.page,
|
||||||
|
page_size: data.page_size,
|
||||||
|
pages: data.pages
|
||||||
|
},
|
||||||
|
status: 200
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('Error fetching seals:', error);
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to connect to server';
|
||||||
|
return {
|
||||||
|
seals: { items: [], total: 0, page, page_size: pageSize, pages: 0 },
|
||||||
|
error: message,
|
||||||
|
status: 500
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,234 +1,272 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/seals/columns';
|
import { createColumns } from '$lib/components/dashboard/general_catalogs/seals/columns';
|
||||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/seals/create-edit-dialog.svelte';
|
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/seals/create-edit-dialog.svelte';
|
||||||
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
import { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
||||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||||
import { obtenerAtajosListaSellos } from '$lib/config/shortcuts/dashboard/general_catalogs/seal/list';
|
import { obtenerAtajosListaSellos } from '$lib/config/shortcuts/dashboard/general_catalogs/seal/list';
|
||||||
import { sealsApi, type Seal } from '$lib/api/dashboard/a76/general_catalogs/seals';
|
import { sealsApi, type Seal } from '$lib/api/dashboard/a76/general_catalogs/seals';
|
||||||
import { companyStore } from '$lib/stores/company.svelte';
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
import { currentUser, userHasPermission } from '$lib/auth';
|
import { currentUser, userHasPermission } from '$lib/auth';
|
||||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let createDialogOpen = $state(false);
|
let createDialogOpen = $state(false);
|
||||||
let editingItem = $state<Seal | null>(null);
|
let editingItem = $state<Seal | null>(null);
|
||||||
let error = $state<string | null>(data.error || null);
|
let error = $state<string | null>(data.error ?? null);
|
||||||
let status = $state<number>(data.status || 200);
|
let status = $state<number>(data.status || 200);
|
||||||
|
|
||||||
// Permisos
|
// Permisos
|
||||||
const canView = $derived(userHasPermission($currentUser, 'cat_seals.view'));
|
const canView = $derived(userHasPermission($currentUser, 'cat_seals.view'));
|
||||||
const canCreate = $derived(userHasPermission($currentUser, 'cat_seals.create'));
|
const canCreate = $derived(userHasPermission($currentUser, 'cat_seals.create'));
|
||||||
const canEdit = $derived(userHasPermission($currentUser, 'cat_seals.edit'));
|
const canEdit = $derived(userHasPermission($currentUser, 'cat_seals.edit'));
|
||||||
const canDelete = $derived(userHasPermission($currentUser, 'cat_seals.delete'));
|
const canDelete = $derived(userHasPermission($currentUser, 'cat_seals.delete'));
|
||||||
|
|
||||||
const isError = $derived(!canView || status >= 400 || error);
|
const isError = $derived(!canView || status >= 400 || error);
|
||||||
|
|
||||||
// Atajos
|
// Atajos
|
||||||
useShortcuts(
|
useShortcuts(
|
||||||
'Lista Sellos',
|
'Lista Sellos',
|
||||||
obtenerAtajosListaSellos({
|
obtenerAtajosListaSellos({
|
||||||
manejarNuevo: () => {
|
manejarNuevo: () => {
|
||||||
if (!canCreate) return;
|
if (!canCreate) return;
|
||||||
editingItem = null;
|
editingItem = null;
|
||||||
createDialogOpen = true;
|
createDialogOpen = true;
|
||||||
},
|
},
|
||||||
manejarActualizar: () => reloadData()
|
manejarActualizar: () => reloadData()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filtros
|
// Filtros
|
||||||
let searchSeal = $state($page.url.searchParams.get('seal') || '');
|
let searchSeal = $state($page.url.searchParams.get('seal') || '');
|
||||||
|
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
let allItems = $state<Seal[]>(data.items?.items || data.items || []);
|
let allItems = $state<Seal[]>(data.seals?.items || data.seals || []);
|
||||||
let currentPage = $state(data.items?.page || 1);
|
let currentPage = $state(data.seals?.page || 1);
|
||||||
let pageSize = $state(data.items?.page_size || 50);
|
let pageSize = $state(data.seals?.page_size || 50);
|
||||||
let totalItems = $state(data.items?.total || 0);
|
let totalItems = $state(data.seals?.total || 0);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let hasMore = $derived(allItems.length < totalItems);
|
let hasMore = $derived(allItems.length < totalItems);
|
||||||
let selectedIds = $state<(string | number)[]>([]);
|
let selectedIds = $state<(string | number)[]>([]);
|
||||||
const selectedItem = $derived(
|
const selectedItem = $derived(
|
||||||
selectedIds.length === 1
|
selectedIds.length === 1
|
||||||
? allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null
|
? (allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (data.items) {
|
if (data.seals) {
|
||||||
allItems = data.items.items || data.items || [];
|
allItems = data.seals.items || data.seals || [];
|
||||||
currentPage = data.items.page || 1;
|
currentPage = data.seals.page || 1;
|
||||||
totalItems = data.items.total || 0;
|
totalItems = data.seals.total || 0;
|
||||||
pageSize = data.items.page_size || pageSize;
|
pageSize = data.seals.page_size || pageSize;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleSearch() {
|
async function handleSearch() {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(async () => {
|
timeout = setTimeout(async () => {
|
||||||
if (!companyStore.activeCompany) return;
|
if (!companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await sealsApi.list(companyStore.activeCompany.id, {
|
const response = await sealsApi.list(companyStore.activeCompany.id, {
|
||||||
seal: searchSeal || undefined,
|
seal: searchSeal || undefined,
|
||||||
|
|
||||||
page: '1',
|
page: '1',
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
allItems = response.data.items;
|
allItems = response.data.items;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Error aplicando filtros';
|
error = 'Error aplicando filtros';
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL($page.url);
|
const url = new URL($page.url);
|
||||||
if (searchSeal) url.searchParams.set('seal', searchSeal);
|
if (searchSeal) url.searchParams.set('seal', searchSeal);
|
||||||
else url.searchParams.delete('seal');
|
else url.searchParams.delete('seal');
|
||||||
history.replaceState(history.state, '', url);
|
history.replaceState(history.state, '', url);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMore() {
|
async function loadMore() {
|
||||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await sealsApi.list(companyStore.activeCompany.id, {
|
const response = await sealsApi.list(companyStore.activeCompany.id, {
|
||||||
seal: searchSeal || undefined,
|
seal: searchSeal || undefined,
|
||||||
|
|
||||||
page: (currentPage + 1).toString(),
|
page: (currentPage + 1).toString(),
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data?.items) {
|
if (response.data?.items) {
|
||||||
allItems = [...allItems, ...response.data.items];
|
allItems = [...allItems, ...response.data.items];
|
||||||
currentPage += 1;
|
currentPage += 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadData() {
|
async function reloadData() {
|
||||||
if (!companyStore.activeCompany) return;
|
if (!companyStore.activeCompany) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await sealsApi.list(companyStore.activeCompany.id, {
|
const response = await sealsApi.list(companyStore.activeCompany.id, {
|
||||||
seal: searchSeal || undefined,
|
seal: searchSeal || undefined,
|
||||||
|
|
||||||
page: '1',
|
page: '1',
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString()
|
||||||
});
|
});
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
allItems = response.data.items;
|
allItems = response.data.items;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
totalItems = response.data.total;
|
totalItems = response.data.total;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSuccess() {
|
function handleSuccess() {
|
||||||
createDialogOpen = false;
|
createDialogOpen = false;
|
||||||
editingItem = null;
|
editingItem = null;
|
||||||
selectedIds = [];
|
selectedIds = [];
|
||||||
reloadData();
|
reloadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]">
|
<div
|
||||||
<div class="flex flex-none items-center justify-between">
|
class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]"
|
||||||
<div>
|
>
|
||||||
<h1 class="text-2xl font-bold tracking-tight">Sellos</h1>
|
<div class="flex flex-none items-center justify-between">
|
||||||
<p class="text-muted-foreground">Catálogo de Sellos del sistema</p>
|
<div>
|
||||||
</div>
|
<h1 class="text-2xl font-bold tracking-tight">Sellos</h1>
|
||||||
<div class="flex items-center gap-3">
|
<p class="text-muted-foreground">Catálogo de Sellos del sistema</p>
|
||||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
</div>
|
||||||
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
<div class="flex items-center gap-3">
|
||||||
</Button>
|
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||||
{#if !isError && canCreate}
|
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
||||||
<Button class="h-9" onclick={() => { editingItem = null; createDialogOpen = true; }}>
|
</Button>
|
||||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
{#if !isError && canCreate}
|
||||||
</Button>
|
<Button
|
||||||
{/if}
|
class="h-9"
|
||||||
</div>
|
onclick={() => {
|
||||||
</div>
|
editingItem = null;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if isError}
|
{#if isError}
|
||||||
<ErrorState
|
<ErrorState
|
||||||
status={!canView ? 403 : status}
|
status={!canView ? 403 : status}
|
||||||
error={!canView ? 'Permission denied: cat_seals.view' : error || ''}
|
error={!canView ? 'Permission denied: cat_seals.view' : error || ''}
|
||||||
onRetry={reloadData}
|
onRetry={reloadData}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<Card.Title>Listado de Sellos</Card.Title>
|
<Card.Title>Listado de Sellos</Card.Title>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<Input placeholder="Filtrar por sello" bind:value={searchSeal} oninput={handleSearch} class="h-9 w-56 bg-card lg:w-72" />
|
<Input
|
||||||
</div>
|
placeholder="Filtrar por sello"
|
||||||
</div>
|
bind:value={searchSeal}
|
||||||
</Card.Header>
|
oninput={handleSearch}
|
||||||
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
class="h-9 w-56 bg-card lg:w-72"
|
||||||
<div class="h-full overflow-hidden rounded-md border bg-background">
|
/>
|
||||||
<InfiniteDataTable
|
</div>
|
||||||
data={allItems} {columns} {loading} {hasMore} {loadMore}
|
</div>
|
||||||
{selectedIds} onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
</Card.Header>
|
||||||
onRowClick={(row) => selectedIds = selectedIds.includes(row.id) ? [] : [row.id]}
|
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
||||||
onRowDoubleClick={(row) => { if(canEdit) { editingItem = row; createDialogOpen = true; } }}
|
<div class="h-full overflow-hidden rounded-md border bg-background">
|
||||||
/>
|
<InfiniteDataTable
|
||||||
</div>
|
data={allItems}
|
||||||
</Card.Content>
|
{columns}
|
||||||
</Card.Root>
|
{loading}
|
||||||
|
{hasMore}
|
||||||
|
{loadMore}
|
||||||
|
{selectedIds}
|
||||||
|
onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
||||||
|
onRowClick={(row) => (selectedIds = selectedIds.includes(row.id) ? [] : [row.id])}
|
||||||
|
onRowDoubleClick={(row) => {
|
||||||
|
if (canEdit) {
|
||||||
|
editingItem = row;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
<div class="flex-none text-sm text-muted-foreground">
|
<div class="flex-none text-sm text-muted-foreground">
|
||||||
Mostrando {allItems.length} de {totalItems} registros
|
Mostrando {allItems.length} de {totalItems} registros
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed right-0 bottom-0 left-0 z-50 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"
|
||||||
|
>
|
||||||
|
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
{#if canEdit}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => {
|
||||||
|
editingItem = selectedItem;
|
||||||
|
createDialogOpen = true;
|
||||||
|
}}
|
||||||
|
disabled={!selectedItem}
|
||||||
|
>
|
||||||
|
<Pencil size={16} class="mr-2" /> Editar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if canDelete}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={async () => {
|
||||||
|
if (!selectedItem || !companyStore.activeCompany) return;
|
||||||
|
if (confirm('¿Eliminar este registro?')) {
|
||||||
|
await sealsApi.delete(selectedItem.id, companyStore.activeCompany.id);
|
||||||
|
selectedIds = [];
|
||||||
|
reloadData();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!selectedItem}
|
||||||
|
class="text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} class="mr-2" /> Eliminar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="fixed right-0 bottom-0 left-0 z-50 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">
|
<CreateEditDialog bind:open={createDialogOpen} item={editingItem} onSuccess={handleSuccess} />
|
||||||
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
{#if canEdit}
|
|
||||||
<Button variant="outline" size="sm" onclick={() => { editingItem = selectedItem; createDialogOpen = true; }} disabled={!selectedItem}>
|
|
||||||
<Pencil size={16} class="mr-2" /> Editar
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{#if canDelete}
|
|
||||||
<Button variant="outline" size="sm" onclick={async () => {
|
|
||||||
if(!selectedItem || !companyStore.activeCompany) return;
|
|
||||||
if(confirm('¿Eliminar este registro?')) {
|
|
||||||
await sealsApi.delete(selectedItem.id, companyStore.activeCompany.id);
|
|
||||||
selectedIds = [];
|
|
||||||
reloadData();
|
|
||||||
}
|
|
||||||
}} disabled={!selectedItem} class="text-destructive hover:bg-destructive/10">
|
|
||||||
<Trash2 size={16} class="mr-2" /> Eliminar
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<CreateEditDialog bind:open={createDialogOpen} item={editingItem} onSuccess={handleSuccess} />
|
|
||||||
</div>
|
|
||||||
@@ -49,10 +49,10 @@ let searchCode = $state($page.url.searchParams.get('code') || '');
|
|||||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
let allItems = $state<Signature[]>(data.items?.items || data.items || []);
|
let allItems = $state<Signature[]>(data.signatures?.items || data.signatures || []);
|
||||||
let currentPage = $state(data.items?.page || 1);
|
let currentPage = $state(data.signatures?.page || 1);
|
||||||
let pageSize = $state(data.items?.page_size || 50);
|
let pageSize = $state(data.signatures?.page_size || 50);
|
||||||
let totalItems = $state(data.items?.total || 0);
|
let totalItems = $state(data.signatures?.total || 0);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let hasMore = $derived(allItems.length < totalItems);
|
let hasMore = $derived(allItems.length < totalItems);
|
||||||
let selectedIds = $state<(string | number)[]>([]);
|
let selectedIds = $state<(string | number)[]>([]);
|
||||||
@@ -63,11 +63,11 @@ selectedIds.length === 1
|
|||||||
);
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (data.items) {
|
if (data.signatures) {
|
||||||
allItems = data.items.items || data.items || [];
|
allItems = data.signatures.items || data.signatures || [];
|
||||||
currentPage = data.items.page || 1;
|
currentPage = data.signatures.page || 1;
|
||||||
totalItems = data.items.total || 0;
|
totalItems = data.signatures.total || 0;
|
||||||
pageSize = data.items.page_size || pageSize;
|
pageSize = data.signatures.page_size || pageSize;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -409,7 +409,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const response = await countriesApi.list(1, pageSize, searchQuery);
|
const response = await countriesApi.list(1, pageSize, searchQuery as any);
|
||||||
if (!response.error && response.data) {
|
if (!response.error && response.data) {
|
||||||
allItems = response.data.items;
|
allItems = response.data.items;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
error = null;
|
error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await countriesApi.list(currentPage + 1, pageSize, searchQuery);
|
const response = await countriesApi.list(currentPage + 1, pageSize, searchQuery as any);
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
|
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
|||||||
Reference in New Issue
Block a user